From 603be55199edc2e38484fc41fa4e3987feaf2a92 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 8 Jul 2015 11:13:39 +0200 Subject: [PATCH 01/45] Add nullability and error management to GRTManagedStore --- Groot.xcodeproj/project.pbxproj | 6 + Groot/GRTManagedStore.h | 68 +++++++++--- Groot/GRTManagedStore.m | 146 ++++++++++++------------- GrootTests/GRTJSONSerializationTests.m | 2 +- GrootTests/GRTManagedStoreTests.m | 78 +++++++++++++ GrootTests/GRTModels.h | 31 +++--- GrootTests/GRTModels.m | 10 +- 7 files changed, 237 insertions(+), 104 deletions(-) create mode 100644 GrootTests/GRTManagedStoreTests.m diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 00c139f..100801f 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1AF81AA9CA5E00F67403 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */; }; B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B081AA9CAC200F67403 /* GRTConstants.h */; }; @@ -42,6 +43,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStoreTests.m; sourceTree = ""; }; B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AF01AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B4DC1AF11AA9CA5E00F67403 /* Groot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Groot.h; sourceTree = ""; }; @@ -136,6 +138,7 @@ B4DC1AFB1AA9CA5E00F67403 /* GrootTests */ = { isa = PBXGroup; children = ( + B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */, B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */, B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, @@ -303,6 +306,7 @@ B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */, B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */, B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */, + B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -482,6 +486,7 @@ B4DC1B041AA9CA5E00F67403 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; B4DC1B051AA9CA5E00F67403 /* Build configuration list for PBXNativeTarget "GrootTests" */ = { isa = XCConfigurationList; @@ -490,6 +495,7 @@ B4DC1B071AA9CA5E00F67403 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ diff --git a/Groot/GRTManagedStore.h b/Groot/GRTManagedStore.h index ff06a38..7b8d25a 100644 --- a/Groot/GRTManagedStore.h +++ b/Groot/GRTManagedStore.h @@ -1,6 +1,6 @@ // GRTManagedStore.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -30,38 +30,76 @@ /** The persistent store coordinator. */ -@property (strong, nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; +@property (strong, nonatomic, readonly, nonnull) NSPersistentStoreCoordinator *persistentStoreCoordinator; /** The managed object model. */ -@property (strong, nonatomic, readonly) NSManagedObjectModel *managedObjectModel; +@property (strong, nonatomic, readonly, nonnull) NSManagedObjectModel *managedObjectModel; /** - Creates and returns a `GRTManagedStore` that will persist its data in memory. + The URL for this managed store. */ -+ (instancetype)managedStoreWithModel:(NSManagedObjectModel *)managedObjectModel; +@property (copy, nonatomic, readonly, nonnull) NSURL *URL; /** - Creates and returns a `GRTManagedStore` that will persist its data in a temporary file. + Initializes the receiver with the specified location and managed object model. + + This is the designated initializer. + + @param URL The file location of the store. If `nil` the persistent store will be created in memory. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (instancetype)temporaryManagedStore; +- (nullable instancetype)initWithURL:(nullable NSURL *)URL model:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error NS_DESIGNATED_INITIALIZER; /** - Creates and returns a `GRTManagedStore` that will persist its data in the application caches directory. + Initializes a managed store that will persist its data in a discardable cache file. - @param cacheName The file name. + @param cacheName The name of the cache file. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (instancetype)managedStoreWithCacheName:(NSString *)cacheName; +- (nullable instancetype)initWithCacheName:(nonnull NSString *)cacheName model:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; /** - Initializes the receiver with the specified path and managed object model. + Initializes a managed store that will persist its data in memory. - This is the designated initializer. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. + */ +- (nullable instancetype)initWithModel:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed store that will persist its data at a given location. - @param path The persistent store path. If `nil` the persistent store will be created in memory. - @param managedObjectModel The managed object model. If `nil` all models in the current bundle will be used. + @param URL The file location of the store. + @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -- (id)initWithPath:(NSString *)path managedObjectModel:(NSManagedObjectModel *)managedObjectModel; ++ (nullable instancetype)storeWithURL:(nonnull NSURL *)URL error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed store that will persist its data in a discardable cache file. + + @param cacheName The file name. + @param error If an error occurs, upon return contains an NSError object that describes the problem. + */ ++ (nullable instancetype)storeWithCacheName:(nonnull NSString *)cacheName error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed object context for this store. + */ +- (nonnull NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType; @end + +@interface GRTManagedStore (Deprecated) + ++ (nonnull instancetype)managedStoreWithModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithModel:error:"))); + ++ (nonnull instancetype)managedStoreWithCacheName:(nonnull NSString *)cacheName __attribute__((deprecated("Replaced by +storeWithCacheName:error:"))); + +- (nonnull id)initWithPath:(nullable NSString *)path managedObjectModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithURL:model:error:"))); + +@end + diff --git a/Groot/GRTManagedStore.m b/Groot/GRTManagedStore.m index fbc6c09..f3fe844 100644 --- a/Groot/GRTManagedStore.m +++ b/Groot/GRTManagedStore.m @@ -1,6 +1,6 @@ // GRTManagedStore.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,108 +22,106 @@ #import "GRTManagedStore.h" -static NSString *GRTApplicationCachePath() { - static dispatch_once_t onceToken; - static NSString *path; +static NSURL *GRTCachesDirectoryURL(NSError **outError) { + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootURL = [fileManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; - dispatch_once(&onceToken, ^{ - path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; - path = [path stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]; - - NSError *error = nil; - BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path - withIntermediateDirectories:YES - attributes:nil - error:&error]; + if (error != nil) { + if (outError != nil) *outError = error; + return nil; + } + + NSString *bundleIdentifier = [NSBundle bundleForClass:[GRTManagedStore class]].bundleIdentifier; + NSURL *cachesDirectoryURL = [rootURL URLByAppendingPathComponent:bundleIdentifier]; + + if (![fileManager fileExistsAtPath:cachesDirectoryURL.path]) { + [fileManager createDirectoryAtURL:cachesDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error]; - if (!success) { - NSLog(@"%s Error creating the application cache directory: %@", __PRETTY_FUNCTION__, error); + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } - }); + } - return path; + return cachesDirectoryURL; } -@interface GRTManagedStore () - -@property (strong, nonatomic, readwrite) NSPersistentStoreCoordinator *persistentStoreCoordinator; -@property (strong, nonatomic, readwrite) NSManagedObjectModel *managedObjectModel; - -@property (copy, nonatomic) NSString *path; - -@end - @implementation GRTManagedStore -#pragma mark - Properties +- (NSManagedObjectModel * __nonnull)managedObjectModel { + return self.persistentStoreCoordinator.managedObjectModel; +} + +- (NSURL * __nonnull)URL { + NSPersistentStore *store = self.persistentStoreCoordinator.persistentStores[0]; + return store.URL; +} -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - if (!_persistentStoreCoordinator) { - _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel]; - - NSString *storeType = self.path ? NSSQLiteStoreType : NSInMemoryStoreType; - NSURL *storeURL = self.path ? [NSURL fileURLWithPath:self.path] : nil; +- (nullable instancetype)initWithURL:(nullable NSURL *)URL + model:(nonnull NSManagedObjectModel *)managedObjectModel + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + self = [super init]; + + if (self) { + _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel]; + NSString *storeType = (URL != nil ? NSSQLiteStoreType : NSInMemoryStoreType); NSDictionary *options = @{ - NSMigratePersistentStoresAutomaticallyOption: @YES, - NSInferMappingModelAutomaticallyOption: @YES - }; + NSMigratePersistentStoresAutomaticallyOption: @YES, + NSInferMappingModelAutomaticallyOption: @YES + }; NSError *error = nil; - NSPersistentStore *store = [_persistentStoreCoordinator addPersistentStoreWithType:storeType - configuration:nil - URL:storeURL - options:options - error:&error]; - if (!store) { - NSLog(@"%@ Error creating persistent store: %@", self, error); + [_persistentStoreCoordinator addPersistentStoreWithType:storeType + configuration:nil + URL:URL + options:options + error:&error]; + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } } - return _persistentStoreCoordinator; + return self; } -- (NSManagedObjectModel *)managedObjectModel { - if (!_managedObjectModel) { - _managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil]; +- (nullable instancetype)initWithCacheName:(nonnull NSString *)cacheName + model:(nonnull NSManagedObjectModel *)managedObjectModel + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError *error = nil; + NSURL *cachesDirectoryURL = GRTCachesDirectoryURL(&error); + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } - return _managedObjectModel; + NSURL *storeURL = [cachesDirectoryURL URLByAppendingPathComponent:cacheName]; + return [self initWithURL:storeURL model:managedObjectModel error:outError]; } -#pragma mark - Lifecycle - -+ (instancetype)managedStoreWithModel:(NSManagedObjectModel *)managedObjectModel { - return [[self alloc] initWithPath:nil managedObjectModel:managedObjectModel]; +- (nullable instancetype)initWithModel:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [self initWithURL:nil model:managedObjectModel error:outError]; } -+ (instancetype)temporaryManagedStore { - NSString *fileName = [[NSProcessInfo processInfo] globallyUniqueString]; - NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; - - return [[self alloc] initWithPath:path managedObjectModel:nil]; ++ (nullable instancetype)storeWithURL:(nonnull NSURL *)URL error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [[self alloc] initWithURL:URL model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -+ (instancetype)managedStoreWithCacheName:(NSString *)cacheName { - NSParameterAssert(cacheName); - - NSString *path = [GRTApplicationCachePath() stringByAppendingPathComponent:cacheName]; - return [[self alloc] initWithPath:path managedObjectModel:nil]; ++ (nullable instancetype)storeWithCacheName:(nonnull NSString *)cacheName error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [[self alloc] initWithCacheName:cacheName model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -- (id)init { - return [self initWithPath:nil managedObjectModel:nil]; -} - -- (id)initWithPath:(NSString *)path managedObjectModel:(NSManagedObjectModel *)managedObjectModel { - self = [super init]; +- (nonnull NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType { + NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType]; + context.persistentStoreCoordinator = self.persistentStoreCoordinator; - if (self) { - _path = [path copy]; - _managedObjectModel = managedObjectModel; - } - - return self; + return context; } @end diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 64e2cfe..c4f747e 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -27,7 +27,7 @@ - (void)setUp { NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; - self.store = [GRTManagedStore managedStoreWithModel:model]; + self.store = [[GRTManagedStore alloc] initWithModel:model error:nil]; self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; self.context.persistentStoreCoordinator = self.store.persistentStoreCoordinator; diff --git a/GrootTests/GRTManagedStoreTests.m b/GrootTests/GRTManagedStoreTests.m new file mode 100644 index 0000000..61af6eb --- /dev/null +++ b/GrootTests/GRTManagedStoreTests.m @@ -0,0 +1,78 @@ +// +// GRTManagedStoreTests.m +// Groot +// +// Created by Guillermo Gonzalez on 08/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import +#import + +#import "GRTModels.h" + +@interface GRTManagedStoreTests : XCTestCase + +@property (copy, nonatomic, nonnull) NSMutableArray *fileURLs; + +@end + +@implementation GRTManagedStoreTests + +- (NSArray * __nonnull)fileURLs { + if (_fileURLs == nil) { + _fileURLs = [NSMutableArray array]; + } + + return _fileURLs; +} + +- (void)tearDown { + for (NSURL *URL in self.fileURLs) { + [[NSFileManager defaultManager] removeItemAtURL:URL error:nil]; + } + [super tearDown]; +} + +- (void)testInMemoryStore { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithModel:model error:&error]; + XCTAssertNil(error); + + NSPersistentStore *persistentStore = store.persistentStoreCoordinator.persistentStores[0]; + XCTAssertEqual(NSInMemoryStoreType, persistentStore.type); +} + +- (void)testStoreWithCacheName { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithCacheName:@"Test.data" model:model error:&error]; + XCTAssertNil(error); + + NSString *bundleIdentifier = [NSBundle bundleForClass:[GRTManagedStore class]].bundleIdentifier; + NSURL *expectedURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; + XCTAssertNil(error); + + expectedURL = [expectedURL URLByAppendingPathComponent:bundleIdentifier]; + expectedURL = [expectedURL URLByAppendingPathComponent:@"Test.data"]; + XCTAssertEqualObjects(expectedURL, store.URL); + + [self.fileURLs addObject:store.URL]; +} + +- (void)testContextWithConcurrencyType { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithModel:model error:&error]; + XCTAssertNil(error); + + NSManagedObjectContext *context = [store contextWithConcurrencyType:NSMainQueueConcurrencyType]; + XCTAssertEqualObjects(store.persistentStoreCoordinator, context.persistentStoreCoordinator); + XCTAssertEqual(NSMainQueueConcurrencyType, context.concurrencyType); +} + +@end diff --git a/GrootTests/GRTModels.h b/GrootTests/GRTModels.h index d504eae..4af6c50 100644 --- a/GrootTests/GRTModels.h +++ b/GrootTests/GRTModels.h @@ -12,27 +12,32 @@ @interface GRTCharacter : NSManagedObject -@property (nonatomic, retain) NSNumber *identifier; -@property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSString *realName; -@property (nonatomic, retain) NSDate *birthday; -@property (nonatomic, retain) NSOrderedSet *powers; -@property (nonatomic, retain) GRTPublisher *publisher; +@property (nonatomic, retain, nonnull) NSNumber *identifier; +@property (nonatomic, retain, nonnull) NSString *name; +@property (nonatomic, retain, nonnull) NSString *realName; +@property (nonatomic, retain, nullable) NSOrderedSet *powers; +@property (nonatomic, retain, nullable) GRTPublisher *publisher; + +@end + +@interface GRTPublisher : NSManagedObject + +@property (nonatomic, retain, nonnull) NSNumber *identifier; +@property (nonatomic, retain, nonnull) NSString *name; +@property (nonatomic, retain, nullable) NSSet *characters; @end @interface GRTPower : NSManagedObject -@property (nonatomic, retain) NSNumber *identifier; -@property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSSet *characters; +@property (nonatomic, retain, nonnull) NSNumber *identifier; +@property (nonatomic, retain, nonnull) NSString *name; +@property (nonatomic, retain, nullable) NSSet *characters; @end -@interface GRTPublisher : NSManagedObject +@interface NSManagedObjectModel (GrootTests) -@property (nonatomic, retain) NSNumber *identifier; -@property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSSet *characters; ++ (nonnull instancetype)grt_testModel; @end diff --git a/GrootTests/GRTModels.m b/GrootTests/GRTModels.m index ad291f5..a75503e 100644 --- a/GrootTests/GRTModels.m +++ b/GrootTests/GRTModels.m @@ -13,7 +13,6 @@ @implementation GRTCharacter @dynamic identifier; @dynamic name; @dynamic realName; -@dynamic birthday; @dynamic powers; @dynamic publisher; @@ -34,3 +33,12 @@ @implementation GRTPublisher @dynamic characters; @end + +@implementation NSManagedObjectModel (GrootTests) + ++ (nonnull instancetype)grt_testModel { + NSBundle *bundle = [NSBundle bundleForClass:[GRTCharacter class]]; + return [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; +} + +@end From ae7bbaa7b4ad37787bb24bdf45982d6271df0ad6 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 8 Jul 2015 22:38:50 +0200 Subject: [PATCH 02/45] Value transformer helper with nullability annotations --- Groot.xcodeproj/project.pbxproj | 24 ++++++--- Groot/Groot.h | 2 +- Groot/NSValueTransformer+Groot.h | 49 ++++++++++++++++++ Groot/NSValueTransformer+Groot.m | 43 ++++++++++++++++ Groot/{ => Private}/GRTValueTransformer.h | 27 +++------- Groot/{ => Private}/GRTValueTransformer.m | 62 +++++++++-------------- GrootTests/GRTJSONSerializationTests.m | 6 +-- GrootTests/GRTValueTransformerTests.m | 20 ++------ 8 files changed, 145 insertions(+), 88 deletions(-) create mode 100644 Groot/NSValueTransformer+Groot.h create mode 100644 Groot/NSValueTransformer+Groot.m rename Groot/{ => Private}/GRTValueTransformer.h (59%) rename Groot/{ => Private}/GRTValueTransformer.m (50%) diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 100801f..02d6fb8 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -16,8 +16,6 @@ B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */; }; B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */; }; - B4DC1B1F1AA9CAC200F67403 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B4DC1B201AA9CAC200F67403 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */; }; B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */; }; B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */; }; B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */; }; @@ -30,6 +28,10 @@ B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */; }; B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */; }; B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */; }; + B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */; }; + B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */; }; + B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,8 +57,6 @@ B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTJSONSerialization.m; sourceTree = ""; }; B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTManagedStore.h; sourceTree = ""; }; B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStore.m; sourceTree = ""; }; - B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTValueTransformer.h; sourceTree = ""; }; - B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformer.m; sourceTree = ""; }; B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributeDescription+Groot.h"; sourceTree = ""; }; B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributeDescription+Groot.m"; sourceTree = ""; }; B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Groot.h"; sourceTree = ""; }; @@ -70,6 +70,10 @@ B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTModels.m; sourceTree = ""; }; B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformerTests.m; sourceTree = ""; }; B4DC1B2E1AA9CBAD00F67403 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTValueTransformer.h; sourceTree = ""; }; + B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformer.m; sourceTree = ""; }; + B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSValueTransformer+Groot.h"; sourceTree = ""; }; + B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSValueTransformer+Groot.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -119,8 +123,8 @@ B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */, B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */, B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */, - B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */, - B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */, + B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */, + B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */, B4DC1B101AA9CAC200F67403 /* Private */, B4DC1AEF1AA9CA5E00F67403 /* Supporting Files */, ); @@ -160,6 +164,8 @@ B4DC1B101AA9CAC200F67403 /* Private */ = { isa = PBXGroup; children = ( + B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */, + B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */, B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */, B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */, B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */, @@ -180,13 +186,14 @@ buildActionMask = 2147483647; files = ( B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */, + B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */, B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */, - B4DC1B1F1AA9CAC200F67403 /* GRTValueTransformer.h in Headers */, B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */, B4DC1B271AA9CAC200F67403 /* NSPropertyDescription+Groot.h in Headers */, B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */, B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */, B4DC1B1B1AA9CAC200F67403 /* GRTJSONSerialization.h in Headers */, + B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */, B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -289,10 +296,11 @@ files = ( B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */, B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */, + B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */, B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */, B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */, - B4DC1B201AA9CAC200F67403 /* GRTValueTransformer.m in Sources */, B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */, + B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */, B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */, B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */, ); diff --git a/Groot/Groot.h b/Groot/Groot.h index e7e66d2..3009518 100644 --- a/Groot/Groot.h +++ b/Groot/Groot.h @@ -29,5 +29,5 @@ FOUNDATION_EXPORT double GrootVersionNumber; FOUNDATION_EXPORT const unsigned char GrootVersionString[]; #import -#import +#import #import diff --git a/Groot/NSValueTransformer+Groot.h b/Groot/NSValueTransformer+Groot.h new file mode 100644 index 0000000..3502d67 --- /dev/null +++ b/Groot/NSValueTransformer+Groot.h @@ -0,0 +1,49 @@ +// NSValueTransformer+Groot.h +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +typedef __nullable id (^GRTTransformBlock)(__nonnull id value); + +@interface NSValueTransformer (Groot) + +/** + Registers a value transformer with a given name and transform block. + + @param name The name of the transformer. + @param transformBlock The block that performs the transformation. + */ ++ (void)grt_setValueTransformerWithName:(nonnull NSString *)name + transformBlock:(nonnull GRTTransformBlock)transformBlock; + +/** + Registers a reversible value transformer with a given name and transform blocks. + + @param name The name of the transformer. + @param transformBlock The block that performs the forward transformation. + @param reverseTransformBlock The block that performs the reverse transformation. + */ ++ (void)grt_setValueTransformerWithName:(nonnull NSString *)name + transformBlock:(nonnull GRTTransformBlock)transformBlock + reverseTransformBlock:(nonnull GRTTransformBlock)reverseTransformBlock; + +@end diff --git a/Groot/NSValueTransformer+Groot.m b/Groot/NSValueTransformer+Groot.m new file mode 100644 index 0000000..2ccd27f --- /dev/null +++ b/Groot/NSValueTransformer+Groot.m @@ -0,0 +1,43 @@ +// NSValueTransformer+Groot.h +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "NSValueTransformer+Groot.h" +#import "GRTValueTransformer.h" + +@implementation NSValueTransformer (Groot) + ++ (void)grt_setValueTransformerWithName:(nonnull NSString *)name + transformBlock:(nonnull GRTTransformBlock)transformBlock +{ + GRTValueTransformer *valueTransformer = [[GRTValueTransformer alloc] initWithBlock:transformBlock]; + [self setValueTransformer:valueTransformer forName:name]; +} + ++ (void)grt_setValueTransformerWithName:(nonnull NSString *)name + transformBlock:(nonnull GRTTransformBlock)transformBlock + reverseTransformBlock:(nonnull GRTTransformBlock)reverseTransformBlock +{ + GRTReversibleValueTransformer *valueTransformer = [[GRTReversibleValueTransformer alloc] initWithForwardBlock:transformBlock reverseBlock:reverseTransformBlock]; + [self setValueTransformer:valueTransformer forName:name]; +} + +@end diff --git a/Groot/GRTValueTransformer.h b/Groot/Private/GRTValueTransformer.h similarity index 59% rename from Groot/GRTValueTransformer.h rename to Groot/Private/GRTValueTransformer.h index 7fcd255..57e894e 100644 --- a/Groot/GRTValueTransformer.h +++ b/Groot/Private/GRTValueTransformer.h @@ -1,8 +1,6 @@ // GRTValueTransformer.h // -// Copyright (c) 2014 Guillermo Gonzalez -// -// Based on Mantle's MTLValueTransformer, MIT licensed. +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,26 +22,17 @@ #import -typedef id (^GRTValueTransformerBlock)(id value); +typedef __nullable id (^GRTValueTransformerBlock)(__nonnull id value); -/** - Generic block-based value transformer. - */ @interface GRTValueTransformer : NSValueTransformer -/** - Returns a transformer which transforms values using the given block. Reverse transformations will not be allowed. - */ -+ (instancetype)transformerWithBlock:(GRTValueTransformerBlock)block; +- (nonnull instancetype)initWithBlock:(nonnull GRTValueTransformerBlock)block; + +@end -/** - Returns a transformer which transforms values using the given block, for forward or reverse transformations. - */ -+ (instancetype)reversibleTransformerWithBlock:(GRTValueTransformerBlock)block; +@interface GRTReversibleValueTransformer : GRTValueTransformer -/** - Returns a transformer which transforms values using the given blocks. - */ -+ (instancetype)reversibleTransformerWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock; +- (nonnull instancetype)initWithForwardBlock:(nonnull GRTValueTransformerBlock)forwardBlock + reverseBlock:(nonnull GRTValueTransformerBlock)reverseBlock; @end diff --git a/Groot/GRTValueTransformer.m b/Groot/Private/GRTValueTransformer.m similarity index 50% rename from Groot/GRTValueTransformer.m rename to Groot/Private/GRTValueTransformer.m index fea1fbc..d08d1ca 100644 --- a/Groot/GRTValueTransformer.m +++ b/Groot/Private/GRTValueTransformer.m @@ -1,8 +1,6 @@ // GRTValueTransformer.m // -// Copyright (c) 2014 Guillermo Gonzalez -// -// Based on Mantle's MTLValueTransformer, MIT licensed. +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,82 +22,68 @@ #import "GRTValueTransformer.h" -#pragma mark - GRTReversibleValueTransformer - -@interface GRTReversibleValueTransformer : GRTValueTransformer -@end - #pragma mark - GRTValueTransformer @interface GRTValueTransformer () -@property (copy, nonatomic, readonly) GRTValueTransformerBlock forwardBlock; -@property (copy, nonatomic, readonly) GRTValueTransformerBlock reverseBlock; - -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock; +@property (copy, nonatomic, nonnull) GRTValueTransformerBlock transformBlock; @end @implementation GRTValueTransformer -+ (instancetype)transformerWithBlock:(GRTValueTransformerBlock)block { - return [[self alloc] initWithForwardBlock:block reverseBlock:nil]; -} - -+ (instancetype)reversibleTransformerWithBlock:(GRTValueTransformerBlock)block { - return [self reversibleTransformerWithForwardBlock:block reverseBlock:block]; -} - -+ (instancetype)reversibleTransformerWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - return [[GRTReversibleValueTransformer alloc] initWithForwardBlock:forwardBlock reverseBlock:reverseBlock]; -} - -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - NSParameterAssert(forwardBlock); - +- (nonnull instancetype)initWithBlock:(nonnull GRTValueTransformerBlock)block { self = [super init]; - if (self) { - _forwardBlock = [forwardBlock copy]; - _reverseBlock = [reverseBlock copy]; + self.transformBlock = block; } - return self; } #pragma mark - NSValueTransformer + (BOOL)allowsReverseTransformation { - return NO; + return NO; } + (Class)transformedValueClass { - return NSObject.class; + return NSObject.class; } - (id)transformedValue:(id)value { - return self.forwardBlock(value); + return self.transformBlock(value); } @end #pragma mark - GRTReversibleValueTransformer +@interface GRTReversibleValueTransformer () + +@property (copy, nonatomic, nonnull) GRTValueTransformerBlock reverseTransformBlock; + +@end + @implementation GRTReversibleValueTransformer -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - NSParameterAssert(reverseBlock); - return [super initWithForwardBlock:forwardBlock reverseBlock:reverseBlock]; +- (nonnull instancetype)initWithForwardBlock:(nonnull GRTValueTransformerBlock)forwardBlock + reverseBlock:(nonnull GRTValueTransformerBlock)reverseBlock +{ + self = [super initWithBlock:forwardBlock]; + if (self) { + self.reverseTransformBlock = reverseBlock; + } + return self; } #pragma mark - NSValueTransformer + (BOOL)allowsReverseTransformation { - return YES; + return YES; } - (id)reverseTransformedValue:(id)value { - return self.reverseBlock(value); + return self.reverseTransformBlock(value); } @end diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index c4f747e..cacfbc3 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -31,17 +31,15 @@ - (void)setUp { self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; self.context.persistentStoreCoordinator = self.store.persistentStoreCoordinator; - NSValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithForwardBlock:^id(NSString *value) { + [NSValueTransformer grt_setValueTransformerWithName:@"GRTTestTransformer" transformBlock:^id(NSString *value) { if (value) { return @([value integerValue]); } return nil; - } reverseBlock:^id(NSNumber *value) { + } reverseTransformBlock:^id(NSNumber *value) { return [value stringValue]; }]; - - [NSValueTransformer setValueTransformer:transformer forName:@"GRTTestTransformer"]; } - (void)tearDown { diff --git a/GrootTests/GRTValueTransformerTests.m b/GrootTests/GRTValueTransformerTests.m index c3b1a23..240a8d9 100644 --- a/GrootTests/GRTValueTransformerTests.m +++ b/GrootTests/GRTValueTransformerTests.m @@ -7,7 +7,7 @@ // #import -#import +#import "GRTValueTransformer.h" @interface GRTValueTransformerTests : XCTestCase @@ -16,7 +16,7 @@ @interface GRTValueTransformerTests : XCTestCase @implementation GRTValueTransformerTests - (void)testTransformerWithBlock { - GRTValueTransformer *transformer = [GRTValueTransformer transformerWithBlock:^id(NSNumber *value) { + GRTValueTransformer *transformer = [[GRTValueTransformer alloc] initWithBlock:^id(NSNumber *value) { return [value stringValue]; }]; @@ -24,22 +24,8 @@ - (void)testTransformerWithBlock { XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the transform block"); } -- (void)testReversibleTransformerWithBlock { - GRTValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithBlock:^id(id value) { - if ([value isKindOfClass:NSNumber.class]) { - return [value stringValue]; - } else { - return @([value integerValue]); - } - }]; - - XCTAssertTrue([transformer.class allowsReverseTransformation], @"should allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the transform block"); - XCTAssertEqualObjects(@42, [transformer reverseTransformedValue:@"42"], @"should call the transform block"); -} - - (void)testReversibleTransformerWithForwardAndReverseBlock { - GRTValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithForwardBlock:^id(NSNumber *value) { + GRTReversibleValueTransformer *transformer = [[GRTReversibleValueTransformer alloc] initWithForwardBlock:^id(NSNumber *value) { return [value stringValue]; } reverseBlock:^id(NSString *value) { return @([value integerValue]); From 9a25201f1c6bee627d44ae7b8f11515a9c121cda Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 8 Jul 2015 23:18:54 +0200 Subject: [PATCH 03/45] Add a type safe value transformer helper --- Groot.xcodeproj/project.pbxproj | 18 ++++++-- Groot/NSValueTransformer+Groot.swift | 58 ++++++++++++++++++++++++ Groot/Private/GRTValueTransformer.m | 10 +++- GrootTests/GRTValueTransformerTests.m | 39 ---------------- GrootTests/NSValueTransformerTests.swift | 49 ++++++++++++++++++++ 5 files changed, 129 insertions(+), 45 deletions(-) create mode 100644 Groot/NSValueTransformer+Groot.swift delete mode 100644 GrootTests/GRTValueTransformerTests.m create mode 100644 GrootTests/NSValueTransformerTests.swift diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 02d6fb8..537077c 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -26,12 +26,13 @@ B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */; }; B4DC1B2F1AA9CBAD00F67403 /* GRTJSONSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */; }; B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */; }; - B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */; }; B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */; }; B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */; }; B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */; }; B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; + B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */; }; + B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -68,12 +69,13 @@ B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTJSONSerializationTests.m; sourceTree = ""; }; B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTModels.h; sourceTree = ""; }; B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTModels.m; sourceTree = ""; }; - B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformerTests.m; sourceTree = ""; }; B4DC1B2E1AA9CBAD00F67403 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTValueTransformer.h; sourceTree = ""; }; B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformer.m; sourceTree = ""; }; B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSValueTransformer+Groot.h"; sourceTree = ""; }; B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSValueTransformer+Groot.m"; sourceTree = ""; }; + B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSValueTransformer+Groot.swift"; sourceTree = ""; }; + B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSValueTransformerTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -125,6 +127,7 @@ B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */, B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */, B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */, + B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */, B4DC1B101AA9CAC200F67403 /* Private */, B4DC1AEF1AA9CA5E00F67403 /* Supporting Files */, ); @@ -146,7 +149,7 @@ B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */, B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, - B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */, + B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */, B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */, B4DC1AFC1AA9CA5E00F67403 /* Supporting Files */, ); @@ -294,6 +297,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */, B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */, B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */, B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */, @@ -310,8 +314,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */, B4DC1B2F1AA9CBAD00F67403 /* GRTJSONSerializationTests.m in Sources */, - B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */, B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */, B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */, B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */, @@ -416,6 +420,7 @@ B4DC1B031AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -425,6 +430,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -432,6 +438,7 @@ B4DC1B041AA9CA5E00F67403 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -448,6 +455,7 @@ B4DC1B061AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", @@ -459,12 +467,14 @@ INFOPLIST_FILE = GrootTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; B4DC1B071AA9CA5E00F67403 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", diff --git a/Groot/NSValueTransformer+Groot.swift b/Groot/NSValueTransformer+Groot.swift new file mode 100644 index 0000000..57d05d8 --- /dev/null +++ b/Groot/NSValueTransformer+Groot.swift @@ -0,0 +1,58 @@ +// NSValueTransformer+Groot.swift +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +public extension NSValueTransformer { + /** + Registers a value transformer with a given name and transform function. + + :param: name The name of the transformer. + :param: transform The function that performs the transformation. + */ + class func setValueTransformerWithName(name: String, transform: (T) -> (U?)) { + grt_setValueTransformerWithName(name) { value in + (value as? T).flatMap { + transform($0) as? AnyObject + } + } + } + + /** + Registers a reversible value transformer with a given name and transform functions. + + :param: name The name of the transformer. + :param: transform The function that performs the forward transformation. + :param: reverseTransform The function that performs the reverse transformation. + */ + class func setValueTransformerWithName(name: String, transform: (T) -> (U?), reverseTransform: (U) -> (T?)) { + grt_setValueTransformerWithName(name, transformBlock: { value in + return (value as? T).flatMap { + transform($0) as? AnyObject + } + }, reverseTransformBlock: { value in + return (value as? U).flatMap { + reverseTransform($0) as? AnyObject + } + }) + } +} diff --git a/Groot/Private/GRTValueTransformer.m b/Groot/Private/GRTValueTransformer.m index d08d1ca..584d64a 100644 --- a/Groot/Private/GRTValueTransformer.m +++ b/Groot/Private/GRTValueTransformer.m @@ -51,7 +51,10 @@ + (Class)transformedValueClass { } - (id)transformedValue:(id)value { - return self.transformBlock(value); + if (value != nil) { + return self.transformBlock(value); + } + return nil; } @end @@ -83,7 +86,10 @@ + (BOOL)allowsReverseTransformation { } - (id)reverseTransformedValue:(id)value { - return self.reverseTransformBlock(value); + if (value != nil) { + return self.reverseTransformBlock(value); + } + return nil; } @end diff --git a/GrootTests/GRTValueTransformerTests.m b/GrootTests/GRTValueTransformerTests.m deleted file mode 100644 index 240a8d9..0000000 --- a/GrootTests/GRTValueTransformerTests.m +++ /dev/null @@ -1,39 +0,0 @@ -// -// GRTValueTransformerTests.m -// Groot -// -// Created by guille on 11/07/14. -// Copyright (c) 2014 Guillermo Gonzalez. All rights reserved. -// - -#import -#import "GRTValueTransformer.h" - -@interface GRTValueTransformerTests : XCTestCase - -@end - -@implementation GRTValueTransformerTests - -- (void)testTransformerWithBlock { - GRTValueTransformer *transformer = [[GRTValueTransformer alloc] initWithBlock:^id(NSNumber *value) { - return [value stringValue]; - }]; - - XCTAssertFalse([transformer.class allowsReverseTransformation], @"should not allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the transform block"); -} - -- (void)testReversibleTransformerWithForwardAndReverseBlock { - GRTReversibleValueTransformer *transformer = [[GRTReversibleValueTransformer alloc] initWithForwardBlock:^id(NSNumber *value) { - return [value stringValue]; - } reverseBlock:^id(NSString *value) { - return @([value integerValue]); - }]; - - XCTAssertTrue([transformer.class allowsReverseTransformation], @"should allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the forward block"); - XCTAssertEqualObjects(@42, [transformer reverseTransformedValue:@"42"], @"should call the reverse block"); -} - -@end diff --git a/GrootTests/NSValueTransformerTests.swift b/GrootTests/NSValueTransformerTests.swift new file mode 100644 index 0000000..5053248 --- /dev/null +++ b/GrootTests/NSValueTransformerTests.swift @@ -0,0 +1,49 @@ +// +// NSValueTransformerTests.swift +// Groot +// +// Created by Guillermo Gonzalez on 08/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +import XCTest +import Groot + +class NSValueTransformerTests: XCTestCase { + + func testValueTransformer() { + func toString(value: Int) -> String? { + return "\(value)" + } + + NSValueTransformer.setValueTransformerWithName("testTransformer", transform: toString) + let transformer = NSValueTransformer(forName: "testTransformer")! + + XCTAssertFalse(transformer.dynamicType.allowsReverseTransformation(), "should not allow reverse transformation") + XCTAssertEqual("42", transformer.transformedValue(42) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.transformedValue("unexpected"), "should handle unsupported values") + } + + func testReversibleValueTransformer() { + func toString(value: Int) -> String? { + return "\(value)" + } + + func toInt(value: String) -> Int? { + return value.toInt() + } + + NSValueTransformer.setValueTransformerWithName("testReversibleTransformer", transform: toString, reverseTransform: toInt) + let transformer = NSValueTransformer(forName: "testReversibleTransformer")! + + XCTAssertTrue(transformer.dynamicType.allowsReverseTransformation(), "should not allow reverse transformation") + XCTAssertEqual("42", transformer.transformedValue(42) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.transformedValue("unexpected"), "should handle unsupported values") + XCTAssertEqual(42, transformer.reverseTransformedValue("42") as! Int, "should call the reverse transform function") + XCTAssertNil(transformer.reverseTransformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.reverseTransformedValue("not a number"), "should handle unsupported values") + } + +} From 0df44e4f21dbb0710881c8cb399f7dadfdd62470 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Thu, 9 Jul 2015 21:47:58 +0200 Subject: [PATCH 04/45] Use NS_ASSUME_NONNULL_XXX macros --- Groot/GRTManagedStore.h | 27 +++++++++++++++------------ Groot/GRTManagedStore.m | 22 +++++++++++++--------- Groot/NSValueTransformer+Groot.h | 16 ++++++++++------ Groot/NSValueTransformer+Groot.m | 14 +++++++++----- Groot/Private/GRTValueTransformer.h | 10 ++++++---- Groot/Private/GRTValueTransformer.m | 14 +++++++++----- 6 files changed, 62 insertions(+), 41 deletions(-) diff --git a/Groot/GRTManagedStore.h b/Groot/GRTManagedStore.h index 7b8d25a..1f171c1 100644 --- a/Groot/GRTManagedStore.h +++ b/Groot/GRTManagedStore.h @@ -22,6 +22,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** Manages a Core Data stack. */ @@ -30,17 +32,17 @@ /** The persistent store coordinator. */ -@property (strong, nonatomic, readonly, nonnull) NSPersistentStoreCoordinator *persistentStoreCoordinator; +@property (strong, nonatomic, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator; /** The managed object model. */ -@property (strong, nonatomic, readonly, nonnull) NSManagedObjectModel *managedObjectModel; +@property (strong, nonatomic, readonly) NSManagedObjectModel *managedObjectModel; /** The URL for this managed store. */ -@property (copy, nonatomic, readonly, nonnull) NSURL *URL; +@property (copy, nonatomic, readonly) NSURL *URL; /** Initializes the receiver with the specified location and managed object model. @@ -51,7 +53,7 @@ @param model The managed object model. @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -- (nullable instancetype)initWithURL:(nullable NSURL *)URL model:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithURL:(nullable NSURL *)URL model:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error NS_DESIGNATED_INITIALIZER; /** Initializes a managed store that will persist its data in a discardable cache file. @@ -60,7 +62,7 @@ @param model The managed object model. @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -- (nullable instancetype)initWithCacheName:(nonnull NSString *)cacheName model:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; +- (nullable instancetype)initWithCacheName:(NSString *)cacheName model:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; /** Initializes a managed store that will persist its data in memory. @@ -68,7 +70,7 @@ @param model The managed object model. @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -- (nullable instancetype)initWithModel:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; +- (nullable instancetype)initWithModel:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; /** Creates and returns a managed store that will persist its data at a given location. @@ -76,7 +78,7 @@ @param URL The file location of the store. @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (nullable instancetype)storeWithURL:(nonnull NSURL *)URL error:(NSError * __nullable * __nullable)error; ++ (nullable instancetype)storeWithURL:(NSURL *)URL error:(NSError * __nullable * __nullable)error; /** Creates and returns a managed store that will persist its data in a discardable cache file. @@ -84,22 +86,23 @@ @param cacheName The file name. @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (nullable instancetype)storeWithCacheName:(nonnull NSString *)cacheName error:(NSError * __nullable * __nullable)error; ++ (nullable instancetype)storeWithCacheName:(NSString *)cacheName error:(NSError * __nullable * __nullable)error; /** Creates and returns a managed object context for this store. */ -- (nonnull NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType; +- (NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType; @end @interface GRTManagedStore (Deprecated) -+ (nonnull instancetype)managedStoreWithModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithModel:error:"))); ++ (instancetype)managedStoreWithModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithModel:error:"))); -+ (nonnull instancetype)managedStoreWithCacheName:(nonnull NSString *)cacheName __attribute__((deprecated("Replaced by +storeWithCacheName:error:"))); ++ (instancetype)managedStoreWithCacheName:(NSString *)cacheName __attribute__((deprecated("Replaced by +storeWithCacheName:error:"))); -- (nonnull id)initWithPath:(nullable NSString *)path managedObjectModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithURL:model:error:"))); +- (id)initWithPath:(nullable NSString *)path managedObjectModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithURL:model:error:"))); @end +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTManagedStore.m b/Groot/GRTManagedStore.m index f3fe844..4d96034 100644 --- a/Groot/GRTManagedStore.m +++ b/Groot/GRTManagedStore.m @@ -22,6 +22,8 @@ #import "GRTManagedStore.h" +NS_ASSUME_NONNULL_BEGIN + static NSURL *GRTCachesDirectoryURL(NSError **outError) { NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; @@ -49,17 +51,17 @@ @implementation GRTManagedStore -- (NSManagedObjectModel * __nonnull)managedObjectModel { +- (NSManagedObjectModel *)managedObjectModel { return self.persistentStoreCoordinator.managedObjectModel; } -- (NSURL * __nonnull)URL { +- (NSURL *)URL { NSPersistentStore *store = self.persistentStoreCoordinator.persistentStores[0]; return store.URL; } - (nullable instancetype)initWithURL:(nullable NSURL *)URL - model:(nonnull NSManagedObjectModel *)managedObjectModel + model:(NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { self = [super init]; @@ -89,8 +91,8 @@ - (nullable instancetype)initWithURL:(nullable NSURL *)URL return self; } -- (nullable instancetype)initWithCacheName:(nonnull NSString *)cacheName - model:(nonnull NSManagedObjectModel *)managedObjectModel +- (nullable instancetype)initWithCacheName:(NSString *)cacheName + model:(NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { NSError *error = nil; @@ -105,19 +107,19 @@ - (nullable instancetype)initWithCacheName:(nonnull NSString *)cacheName return [self initWithURL:storeURL model:managedObjectModel error:outError]; } -- (nullable instancetype)initWithModel:(nonnull NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { +- (nullable instancetype)initWithModel:(NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { return [self initWithURL:nil model:managedObjectModel error:outError]; } -+ (nullable instancetype)storeWithURL:(nonnull NSURL *)URL error:(NSError *__autoreleasing __nullable * __nullable)outError { ++ (nullable instancetype)storeWithURL:(NSURL *)URL error:(NSError *__autoreleasing __nullable * __nullable)outError { return [[self alloc] initWithURL:URL model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -+ (nullable instancetype)storeWithCacheName:(nonnull NSString *)cacheName error:(NSError *__autoreleasing __nullable * __nullable)outError { ++ (nullable instancetype)storeWithCacheName:(NSString *)cacheName error:(NSError *__autoreleasing __nullable * __nullable)outError { return [[self alloc] initWithCacheName:cacheName model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -- (nonnull NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType { +- (NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType { NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType]; context.persistentStoreCoordinator = self.persistentStoreCoordinator; @@ -125,3 +127,5 @@ - (nonnull NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectC } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.h b/Groot/NSValueTransformer+Groot.h index 3502d67..3c587d3 100644 --- a/Groot/NSValueTransformer+Groot.h +++ b/Groot/NSValueTransformer+Groot.h @@ -22,7 +22,9 @@ #import -typedef __nullable id (^GRTTransformBlock)(__nonnull id value); +NS_ASSUME_NONNULL_BEGIN + +typedef __nullable id (^GRTTransformBlock)(id value); @interface NSValueTransformer (Groot) @@ -32,8 +34,8 @@ typedef __nullable id (^GRTTransformBlock)(__nonnull id value); @param name The name of the transformer. @param transformBlock The block that performs the transformation. */ -+ (void)grt_setValueTransformerWithName:(nonnull NSString *)name - transformBlock:(nonnull GRTTransformBlock)transformBlock; ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock; /** Registers a reversible value transformer with a given name and transform blocks. @@ -42,8 +44,10 @@ typedef __nullable id (^GRTTransformBlock)(__nonnull id value); @param transformBlock The block that performs the forward transformation. @param reverseTransformBlock The block that performs the reverse transformation. */ -+ (void)grt_setValueTransformerWithName:(nonnull NSString *)name - transformBlock:(nonnull GRTTransformBlock)transformBlock - reverseTransformBlock:(nonnull GRTTransformBlock)reverseTransformBlock; ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock + reverseTransformBlock:(__nullable id (^)(id value))reverseTransformBlock; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.m b/Groot/NSValueTransformer+Groot.m index 2ccd27f..c040b54 100644 --- a/Groot/NSValueTransformer+Groot.m +++ b/Groot/NSValueTransformer+Groot.m @@ -23,21 +23,25 @@ #import "NSValueTransformer+Groot.h" #import "GRTValueTransformer.h" +NS_ASSUME_NONNULL_BEGIN + @implementation NSValueTransformer (Groot) -+ (void)grt_setValueTransformerWithName:(nonnull NSString *)name - transformBlock:(nonnull GRTTransformBlock)transformBlock ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock { GRTValueTransformer *valueTransformer = [[GRTValueTransformer alloc] initWithBlock:transformBlock]; [self setValueTransformer:valueTransformer forName:name]; } -+ (void)grt_setValueTransformerWithName:(nonnull NSString *)name - transformBlock:(nonnull GRTTransformBlock)transformBlock - reverseTransformBlock:(nonnull GRTTransformBlock)reverseTransformBlock ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock + reverseTransformBlock:(__nullable id (^)(id value))reverseTransformBlock { GRTReversibleValueTransformer *valueTransformer = [[GRTReversibleValueTransformer alloc] initWithForwardBlock:transformBlock reverseBlock:reverseTransformBlock]; [self setValueTransformer:valueTransformer forName:name]; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/GRTValueTransformer.h b/Groot/Private/GRTValueTransformer.h index 57e894e..8bc3d09 100644 --- a/Groot/Private/GRTValueTransformer.h +++ b/Groot/Private/GRTValueTransformer.h @@ -22,17 +22,19 @@ #import -typedef __nullable id (^GRTValueTransformerBlock)(__nonnull id value); +NS_ASSUME_NONNULL_BEGIN @interface GRTValueTransformer : NSValueTransformer -- (nonnull instancetype)initWithBlock:(nonnull GRTValueTransformerBlock)block; +- (instancetype)initWithBlock:(__nullable id (^)(id value))block; @end @interface GRTReversibleValueTransformer : GRTValueTransformer -- (nonnull instancetype)initWithForwardBlock:(nonnull GRTValueTransformerBlock)forwardBlock - reverseBlock:(nonnull GRTValueTransformerBlock)reverseBlock; +- (instancetype)initWithForwardBlock:(__nullable id (^)(id value))forwardBlock + reverseBlock:(__nullable id (^)(id value))reverseBlock; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/GRTValueTransformer.m b/Groot/Private/GRTValueTransformer.m index 584d64a..0b53fdf 100644 --- a/Groot/Private/GRTValueTransformer.m +++ b/Groot/Private/GRTValueTransformer.m @@ -22,17 +22,19 @@ #import "GRTValueTransformer.h" +NS_ASSUME_NONNULL_BEGIN + #pragma mark - GRTValueTransformer @interface GRTValueTransformer () -@property (copy, nonatomic, nonnull) GRTValueTransformerBlock transformBlock; +@property (copy, nonatomic) __nullable id (^transformBlock)(id); @end @implementation GRTValueTransformer -- (nonnull instancetype)initWithBlock:(nonnull GRTValueTransformerBlock)block { +- (instancetype)initWithBlock:(__nullable id (^)(id value))block { self = [super init]; if (self) { self.transformBlock = block; @@ -63,14 +65,14 @@ - (id)transformedValue:(id)value { @interface GRTReversibleValueTransformer () -@property (copy, nonatomic, nonnull) GRTValueTransformerBlock reverseTransformBlock; +@property (copy, nonatomic) __nullable id (^reverseTransformBlock)(id); @end @implementation GRTReversibleValueTransformer -- (nonnull instancetype)initWithForwardBlock:(nonnull GRTValueTransformerBlock)forwardBlock - reverseBlock:(nonnull GRTValueTransformerBlock)reverseBlock +- (instancetype)initWithForwardBlock:(__nullable id (^)(id value))forwardBlock + reverseBlock:(__nullable id (^)(id value))reverseBlock { self = [super initWithBlock:forwardBlock]; if (self) { @@ -93,3 +95,5 @@ - (id)reverseTransformedValue:(id)value { } @end + +NS_ASSUME_NONNULL_END From 159a665de8befd0b23154a70f339d4dc7768ae2a Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Thu, 9 Jul 2015 22:00:53 +0200 Subject: [PATCH 05/45] Add error constants --- Groot.xcodeproj/project.pbxproj | 8 ++++++++ Groot/GRTError.h | 30 ++++++++++++++++++++++++++++++ Groot/GRTError.m | 25 +++++++++++++++++++++++++ Groot/Groot.h | 1 + 4 files changed, 64 insertions(+) create mode 100644 Groot/GRTError.h create mode 100644 Groot/GRTError.m diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 537077c..d3bb005 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1AF81AA9CA5E00F67403 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */; }; @@ -46,6 +48,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B46200C01B4F07E4003B3B69 /* GRTError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTError.h; sourceTree = ""; }; + B46200C11B4F07E4003B3B69 /* GRTError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTError.m; sourceTree = ""; }; B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStoreTests.m; sourceTree = ""; }; B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AF01AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -119,6 +123,8 @@ isa = PBXGroup; children = ( B4DC1AF11AA9CA5E00F67403 /* Groot.h */, + B46200C01B4F07E4003B3B69 /* GRTError.h */, + B46200C11B4F07E4003B3B69 /* GRTError.m */, B4DC1B081AA9CAC200F67403 /* GRTConstants.h */, B4DC1B091AA9CAC200F67403 /* GRTConstants.m */, B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */, @@ -188,6 +194,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */, B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */, B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */, B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */, @@ -307,6 +314,7 @@ B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */, B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */, B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */, + B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Groot/GRTError.h b/Groot/GRTError.h new file mode 100644 index 0000000..b7f78f1 --- /dev/null +++ b/Groot/GRTError.h @@ -0,0 +1,30 @@ +// GRTError.h +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +extern NSString * const GRTErrorDomain; + +typedef NS_ENUM(NSInteger, GRTError) { + GRTErrorInvalidJSONObject, + GRTErrorIdentityNotFound +}; diff --git a/Groot/GRTError.m b/Groot/GRTError.m new file mode 100644 index 0000000..dc5c349 --- /dev/null +++ b/Groot/GRTError.m @@ -0,0 +1,25 @@ +// GRTError.m +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "GRTError.h" + +NSString * const GRTErrorDomain = @"com.groot.error"; diff --git a/Groot/Groot.h b/Groot/Groot.h index 3009518..a56d445 100644 --- a/Groot/Groot.h +++ b/Groot/Groot.h @@ -28,6 +28,7 @@ FOUNDATION_EXPORT double GrootVersionNumber; //! Project version string for Groot. FOUNDATION_EXPORT const unsigned char GrootVersionString[]; +#import #import #import #import From 023f24a1ee350e98c2ca5efe2cd00ab566710849 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Thu, 9 Jul 2015 22:46:46 +0200 Subject: [PATCH 06/45] Refactor attribute and property helpers --- Groot.xcodeproj/project.pbxproj | 4 +- Groot/GRTConstants.h | 2 - Groot/GRTConstants.m | 2 - Groot/Private/NSAttributeDescription+Groot.h | 21 ++++++++- Groot/Private/NSAttributeDescription+Groot.m | 46 +++++++++++++++++--- Groot/Private/NSPropertyDescription+Groot.h | 21 ++++++++- Groot/Private/NSPropertyDescription+Groot.m | 23 +++++----- 7 files changed, 94 insertions(+), 25 deletions(-) diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index d3bb005..776d2e6 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -175,14 +175,14 @@ children = ( B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */, B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */, + B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */, + B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */, B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */, B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */, B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */, B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */, B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */, B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */, - B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */, - B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */, ); path = Private; sourceTree = ""; diff --git a/Groot/GRTConstants.h b/Groot/GRTConstants.h index 9b6b7d9..c5f85c5 100644 --- a/Groot/GRTConstants.h +++ b/Groot/GRTConstants.h @@ -22,6 +22,4 @@ #import -extern NSString * const GRTJSONKeyPathKey; -extern NSString * const GRTJSONTransformerNameKey; extern NSString * const GRTIdentityAttributeKey; diff --git a/Groot/GRTConstants.m b/Groot/GRTConstants.m index e916eb7..53c56aa 100644 --- a/Groot/GRTConstants.m +++ b/Groot/GRTConstants.m @@ -22,6 +22,4 @@ #import "GRTConstants.h" -NSString * const GRTJSONKeyPathKey = @"JSONKeyPath"; -NSString * const GRTJSONTransformerNameKey = @"JSONTransformerName"; NSString * const GRTIdentityAttributeKey = @"identityAttribute"; diff --git a/Groot/Private/NSAttributeDescription+Groot.h b/Groot/Private/NSAttributeDescription+Groot.h index 952673e..1ce4d9c 100644 --- a/Groot/Private/NSAttributeDescription+Groot.h +++ b/Groot/Private/NSAttributeDescription+Groot.h @@ -1,6 +1,6 @@ // NSAttributeDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,25 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSAttributeDescription (Groot) -- (NSValueTransformer *)grt_JSONTransformer; +/** + The value transformer for this attribute. + */ +- (nullable NSValueTransformer *)grt_JSONTransformer; + +/** + Returns the value for this attribute in a given JSON object. + */ +- (nullable id)grt_valueInJSONObject:(NSDictionary *)object; + +/** + Returns all the values for this attribute in a given JSON array. + */ +- (NSArray *)grt_valuesInJSONArray:(NSArray *)array; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSAttributeDescription+Groot.m b/Groot/Private/NSAttributeDescription+Groot.m index 9bf6967..7afc7e1 100644 --- a/Groot/Private/NSAttributeDescription+Groot.m +++ b/Groot/Private/NSAttributeDescription+Groot.m @@ -1,6 +1,6 @@ // NSAttributeDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,13 +21,49 @@ // THE SOFTWARE. #import "NSAttributeDescription+Groot.h" -#import "GRTConstants.h" +#import "NSPropertyDescription+Groot.h" @implementation NSAttributeDescription (Groot) -- (NSValueTransformer *)grt_JSONTransformer { - NSString *name = self.userInfo[GRTJSONTransformerNameKey]; - return name ? [NSValueTransformer valueTransformerForName:name] : nil; +- (nullable NSValueTransformer *)grt_JSONTransformer { + NSString *name = self.userInfo[@"JSONTransformerName"]; + return name != nil ? [NSValueTransformer valueTransformerForName:name] : nil; +} + +- (nullable id)grt_valueInJSONObject:(NSDictionary * __nonnull)object { + id value = [self grt_rawValueInJSONObject:object]; + + if (value != nil) { + if (value == [NSNull null]) { + return nil; + } + + NSValueTransformer *transformer = [self grt_JSONTransformer]; + + if (transformer != nil) { + return [transformer transformedValue:value]; + } + + return value; + } + + return nil; +} + +- (NSArray * __nonnull)grt_valuesInJSONArray:(NSArray * __nonnull)array { + NSMutableArray *values = [NSMutableArray arrayWithCapacity:array.count]; + + for (NSDictionary *object in array) { + if ([object isKindOfClass:[NSDictionary class]]) { + id value = [self grt_valueInJSONObject:object]; + + if (value != nil) { + [values addObject:value]; + } + } + } + + return values; } @end diff --git a/Groot/Private/NSPropertyDescription+Groot.h b/Groot/Private/NSPropertyDescription+Groot.h index fc3f24c..3d6961a 100644 --- a/Groot/Private/NSPropertyDescription+Groot.h +++ b/Groot/Private/NSPropertyDescription+Groot.h @@ -1,6 +1,6 @@ // NSPropertyDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,25 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSPropertyDescription (Groot) -- (NSString *)grt_JSONKeyPath; +/** + The JSON key path. + */ +- (nullable NSString *)grt_JSONKeyPath; + +/** + Returns `true` if this property should participate in the JSON serialization process. + */ +- (BOOL)grt_JSONSerializable; + +/** + Returns the untransformed raw value for this property in a given JSON object. + */ +- (nullable id)grt_rawValueInJSONObject:(NSDictionary *)object; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSPropertyDescription+Groot.m b/Groot/Private/NSPropertyDescription+Groot.m index 5320739..0da1414 100644 --- a/Groot/Private/NSPropertyDescription+Groot.m +++ b/Groot/Private/NSPropertyDescription+Groot.m @@ -1,6 +1,6 @@ // NSPropertyDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,22 +21,25 @@ // THE SOFTWARE. #import "NSPropertyDescription+Groot.h" -#import "GRTConstants.h" -static BOOL GRTIsNullKeyPath(NSString *keyPath) { - return [keyPath isEqual:NSNull.null] || [keyPath isEqualToString:@"null"]; +@implementation NSPropertyDescription (Groot) + +- (nullable NSString *)grt_JSONKeyPath { + return self.userInfo[@"JSONKeyPath"]; } -@implementation NSPropertyDescription (Groot) +- (BOOL)grt_JSONSerializable { + return [self grt_JSONKeyPath] != nil; +} -- (NSString *)grt_JSONKeyPath { - NSString *JSONKeyPath = self.userInfo[GRTJSONKeyPathKey]; +- (nullable id)grt_rawValueInJSONObject:(NSDictionary * __nonnull)object { + NSString *keyPath = [self grt_JSONKeyPath]; - if (GRTIsNullKeyPath(JSONKeyPath)) { - return nil; + if (keyPath != nil) { + return [object valueForKeyPath:keyPath]; } - return JSONKeyPath ? : self.name; + return nil; } @end From cff54e69c420d69a8930a21a31ded463114b21e3 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Mon, 13 Jul 2015 15:42:10 +0200 Subject: [PATCH 07/45] Rename some methods for consistency --- Groot/Private/NSAttributeDescription+Groot.h | 2 +- Groot/Private/NSAttributeDescription+Groot.m | 6 +++--- Groot/Private/NSPropertyDescription+Groot.h | 2 +- Groot/Private/NSPropertyDescription+Groot.m | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Groot/Private/NSAttributeDescription+Groot.h b/Groot/Private/NSAttributeDescription+Groot.h index 1ce4d9c..831871f 100644 --- a/Groot/Private/NSAttributeDescription+Groot.h +++ b/Groot/Private/NSAttributeDescription+Groot.h @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN /** Returns the value for this attribute in a given JSON object. */ -- (nullable id)grt_valueInJSONObject:(NSDictionary *)object; +- (nullable id)grt_valueInJSONDictionary:(NSDictionary *)dictionary; /** Returns all the values for this attribute in a given JSON array. diff --git a/Groot/Private/NSAttributeDescription+Groot.m b/Groot/Private/NSAttributeDescription+Groot.m index 7afc7e1..70b631d 100644 --- a/Groot/Private/NSAttributeDescription+Groot.m +++ b/Groot/Private/NSAttributeDescription+Groot.m @@ -30,8 +30,8 @@ - (nullable NSValueTransformer *)grt_JSONTransformer { return name != nil ? [NSValueTransformer valueTransformerForName:name] : nil; } -- (nullable id)grt_valueInJSONObject:(NSDictionary * __nonnull)object { - id value = [self grt_rawValueInJSONObject:object]; +- (nullable id)grt_valueInJSONDictionary:(NSDictionary * __nonnull)dictionary { + id value = [self grt_rawValueInJSONDictionary:dictionary]; if (value != nil) { if (value == [NSNull null]) { @@ -55,7 +55,7 @@ - (NSArray * __nonnull)grt_valuesInJSONArray:(NSArray * __nonnull)array { for (NSDictionary *object in array) { if ([object isKindOfClass:[NSDictionary class]]) { - id value = [self grt_valueInJSONObject:object]; + id value = [self grt_valueInJSONDictionary:object]; if (value != nil) { [values addObject:value]; diff --git a/Groot/Private/NSPropertyDescription+Groot.h b/Groot/Private/NSPropertyDescription+Groot.h index 3d6961a..1678cdc 100644 --- a/Groot/Private/NSPropertyDescription+Groot.h +++ b/Groot/Private/NSPropertyDescription+Groot.h @@ -39,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN /** Returns the untransformed raw value for this property in a given JSON object. */ -- (nullable id)grt_rawValueInJSONObject:(NSDictionary *)object; +- (nullable id)grt_rawValueInJSONDictionary:(NSDictionary *)object; @end diff --git a/Groot/Private/NSPropertyDescription+Groot.m b/Groot/Private/NSPropertyDescription+Groot.m index 0da1414..a7e67b7 100644 --- a/Groot/Private/NSPropertyDescription+Groot.m +++ b/Groot/Private/NSPropertyDescription+Groot.m @@ -32,11 +32,11 @@ - (BOOL)grt_JSONSerializable { return [self grt_JSONKeyPath] != nil; } -- (nullable id)grt_rawValueInJSONObject:(NSDictionary * __nonnull)object { +- (nullable id)grt_rawValueInJSONDictionary:(NSDictionary * __nonnull)dictionary { NSString *keyPath = [self grt_JSONKeyPath]; if (keyPath != nil) { - return [object valueForKeyPath:keyPath]; + return [dictionary valueForKeyPath:keyPath]; } return nil; From 24fad3ec5d2ff40c84c3119390db51f1b83d37fb Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Mon, 13 Jul 2015 15:57:08 +0200 Subject: [PATCH 08/45] Remove unused category --- Groot.xcodeproj/project.pbxproj | 8 ------ Groot/Private/NSDictionary+Groot.h | 31 -------------------- Groot/Private/NSDictionary+Groot.m | 46 ------------------------------ 3 files changed, 85 deletions(-) delete mode 100644 Groot/Private/NSDictionary+Groot.h delete mode 100644 Groot/Private/NSDictionary+Groot.m diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 776d2e6..e34f780 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -20,8 +20,6 @@ B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */; }; B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */; }; B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */; }; - B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */; }; - B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */; }; B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */; }; B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */; }; B4DC1B271AA9CAC200F67403 /* NSPropertyDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */; }; @@ -64,8 +62,6 @@ B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStore.m; sourceTree = ""; }; B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributeDescription+Groot.h"; sourceTree = ""; }; B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributeDescription+Groot.m"; sourceTree = ""; }; - B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Groot.h"; sourceTree = ""; }; - B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Groot.m"; sourceTree = ""; }; B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSEntityDescription+Groot.h"; sourceTree = ""; }; B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSEntityDescription+Groot.m"; sourceTree = ""; }; B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSPropertyDescription+Groot.h"; sourceTree = ""; }; @@ -179,8 +175,6 @@ B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */, B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */, B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */, - B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */, - B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */, B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */, B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */, ); @@ -195,7 +189,6 @@ buildActionMask = 2147483647; files = ( B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */, - B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */, B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */, B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */, B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */, @@ -312,7 +305,6 @@ B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */, B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */, B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */, - B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */, B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */, B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */, ); diff --git a/Groot/Private/NSDictionary+Groot.h b/Groot/Private/NSDictionary+Groot.h deleted file mode 100644 index 4d28543..0000000 --- a/Groot/Private/NSDictionary+Groot.h +++ /dev/null @@ -1,31 +0,0 @@ -// NSDictionary+Groot.h -// -// Copyright (c) 2014 Guillermo Gonzalez -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#import - -@class NSAttributeDescription; - -@interface NSDictionary (Groot) - -- (id)grt_valueForAttribute:(NSAttributeDescription *)attribute; - -@end diff --git a/Groot/Private/NSDictionary+Groot.m b/Groot/Private/NSDictionary+Groot.m deleted file mode 100644 index be84bb6..0000000 --- a/Groot/Private/NSDictionary+Groot.m +++ /dev/null @@ -1,46 +0,0 @@ -// NSDictionary+Groot.m -// -// Copyright (c) 2014 Guillermo Gonzalez -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -#import "NSDictionary+Groot.h" -#import "NSPropertyDescription+Groot.h" -#import "NSAttributeDescription+Groot.h" - -@implementation NSDictionary (Groot) - -- (id)grt_valueForAttribute:(NSAttributeDescription *)attribute { - id value = [self valueForKeyPath:[attribute grt_JSONKeyPath]]; - - if ([value isEqual:NSNull.null]) { - value = nil; - } - - if (value != nil) { - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - if (transformer) { - value = [transformer transformedValue:value]; - } - } - - return value; -} - -@end From c39f98e1eae2ff1e1557292df244bf652981ea5b Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Mon, 13 Jul 2015 16:20:33 +0200 Subject: [PATCH 09/45] Refactoring serialization API --- Groot.xcodeproj/project.pbxproj | 16 +- Groot/GRTJSONSerialization.h | 119 +++-- Groot/GRTJSONSerialization.m | 408 ++---------------- Groot/Private/NSEntityDescription+Groot.h | 2 - Groot/Private/NSEntityDescription+Groot.m | 20 +- .../NSManagedObject+Groot.h} | 32 +- .../NSManagedObject+Groot.m} | 42 +- GrootTests/GRTJSONSerializationTests.m | 24 +- 8 files changed, 166 insertions(+), 497 deletions(-) rename Groot/{GRTConstants.m => Private/NSManagedObject+Groot.h} (58%) rename Groot/{GRTConstants.h => Private/NSManagedObject+Groot.m} (55%) diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index e34f780..525956b 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -9,11 +9,11 @@ /* Begin PBXBuildFile section */ B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; + B475B4101B53FCE1001F29FE /* NSManagedObject+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */; }; + B475B4111B53FCE1001F29FE /* NSManagedObject+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */; }; B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1AF81AA9CA5E00F67403 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */; }; - B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B081AA9CAC200F67403 /* GRTConstants.h */; }; - B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B091AA9CAC200F67403 /* GRTConstants.m */; }; B4DC1B1B1AA9CAC200F67403 /* GRTJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */; }; B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -48,14 +48,14 @@ /* Begin PBXFileReference section */ B46200C01B4F07E4003B3B69 /* GRTError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTError.h; sourceTree = ""; }; B46200C11B4F07E4003B3B69 /* GRTError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTError.m; sourceTree = ""; }; + B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+Groot.h"; sourceTree = ""; }; + B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+Groot.m"; sourceTree = ""; }; B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStoreTests.m; sourceTree = ""; }; B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AF01AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B4DC1AF11AA9CA5E00F67403 /* Groot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Groot.h; sourceTree = ""; }; B4DC1AF71AA9CA5E00F67403 /* GrootTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GrootTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AFD1AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B4DC1B081AA9CAC200F67403 /* GRTConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTConstants.h; sourceTree = ""; }; - B4DC1B091AA9CAC200F67403 /* GRTConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTConstants.m; sourceTree = ""; }; B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTJSONSerialization.h; sourceTree = ""; }; B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTJSONSerialization.m; sourceTree = ""; }; B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTManagedStore.h; sourceTree = ""; }; @@ -121,8 +121,6 @@ B4DC1AF11AA9CA5E00F67403 /* Groot.h */, B46200C01B4F07E4003B3B69 /* GRTError.h */, B46200C11B4F07E4003B3B69 /* GRTError.m */, - B4DC1B081AA9CAC200F67403 /* GRTConstants.h */, - B4DC1B091AA9CAC200F67403 /* GRTConstants.m */, B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */, B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */, B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */, @@ -177,6 +175,8 @@ B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */, B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */, B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */, + B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */, + B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */, ); path = Private; sourceTree = ""; @@ -191,11 +191,11 @@ B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */, B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */, B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */, - B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */, B4DC1B271AA9CAC200F67403 /* NSPropertyDescription+Groot.h in Headers */, B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */, B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */, B4DC1B1B1AA9CAC200F67403 /* GRTJSONSerialization.h in Headers */, + B475B4101B53FCE1001F29FE /* NSManagedObject+Groot.h in Headers */, B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */, B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */, ); @@ -303,9 +303,9 @@ B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */, B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */, B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */, - B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */, B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */, B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */, + B475B4111B53FCE1001F29FE /* NSManagedObject+Groot.m in Sources */, B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Groot/GRTJSONSerialization.h b/Groot/GRTJSONSerialization.h index 4d5ba41..b1dccf2 100644 --- a/Groot/GRTJSONSerialization.h +++ b/Groot/GRTJSONSerialization.h @@ -1,6 +1,6 @@ // GRTJSONSerialization.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,91 +22,49 @@ #import -extern NSString * const GRTJSONSerializationErrorDomain; -extern const NSInteger GRTJSONSerializationErrorInvalidJSONObject; +NS_ASSUME_NONNULL_BEGIN /** Converts JSON dictionaries and JSON arrays to and from Managed Objects. - - The serialization process can be customized by adding certain information to the user dictionary - available in Core Data entities, attributes and relationships: - - You can specify how an attribute or a relationship is mapped to JSON with the `JSONKeyPath` key. If - this key is not present, the attribute name will be used. If `JSONKeyPath` is associated with `@"null"` - or `NSNull` then the attribute or relationship will not participate in JSON serialization. - - Use `JSONTransformerName` to specify the name of the value transformer that will be used to convert - the JSON value for an attribute. If the specified transformer is reversible, it will also be used - to convert the attribute value back to JSON. - - The **merge** methods `mergeObjectForEntityName:fromJSONDictionary:inManagedObjectContext:error:` and - `mergeObjectsForEntityName:fromJSONArray:inManagedObjectContext:error:` need to know when a managed - object already exist when converting from JSON. You can specify which attribute makes an object unique - by using the `identityAttribute` option. Note that this option must be added to the **Entity** user - dictionary. - - Note that the user dictionary can be manipulated directly in the Core Data Model Editor. */ @interface GRTJSONSerialization : NSObject /** - Creates a managed object from a JSON dictionary. - - This method converts the specified JSON dictionary into a managed object of a given entity. + Creates or updates a set of managed objects from JSON data. @param entityName The name of an entity. - @param JSONDictionary A dictionary representing JSON data. This should match the format returned - by `NSJSONSerialization`. - @param context The context into which to insert the created managed object. - @param error If an error occurs, upon return contains an NSError object that describes the problem. - - @return A managed object, or `nil` if an error occurs. - */ -+ (id)insertObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; - -/** - Creates set of managed objects from a JSON array. - - This method converts the specified JSON array into a set of managed objects of a given entity. - - @param entityName The name of an entity. - @param JSONArray An array of dictionaries representing JSON data. This should match the format - returned by `NSJSONSerialization`. - @param context The context into which to insert the created managed object. + @param data A data object containing JSON data. + @param context The context into which to fetch or insert the managed objects. @param error If an error occurs, upon return contains an NSError object that describes the problem. @return An array of managed objects, or `nil` if an error occurs. */ -+ (NSArray *)insertObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONData:(NSData *)data + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Creates or updates a managed object from a JSON dictionary. - This method will perform a fetch request for an object matching the given JSON dictionary, using - the identity attribute specified in the entity's user info. If a match is found, then it will be - updated with the given JSON dictionary, otherwise a new object will be created. - - Note that this method will throw an exception if the given entity has no identity attribute defined. + This method converts the specified JSON dictionary into a managed object of a given entity. @param entityName The name of an entity. @param JSONDictionary A dictionary representing JSON data. This should match the format returned - by `NSJSONSerialization`. - @param context The context into which to fetch or insert the managed object. + by `NSJSONSerialization`. + @param context The context into which to fetch or insert the managed objects. @param error If an error occurs, upon return contains an NSError object that describes the problem. @return A managed object, or `nil` if an error occurs. */ -+ (id)mergeObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable id)objectWithEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Creates or updates a set of managed objects from a JSON array. - This method will perform a **single** fetch request for objects matching the given JSON array, - using the identity attribute specified in the entity's user info. Matching objects will be updated, - and the rest will be created. - - Note that this method will throw an exception if the given entity has no identity attribute defined. - @param entityName The name of an entity. @param JSONArray An array representing JSON data. This should match the format returned by `NSJSONSerialization`. @@ -115,24 +73,57 @@ extern const NSInteger GRTJSONSerializationErrorInvalidJSONObject; @return An array of managed objects, or `nil` if an error occurs. */ -+ (NSArray *)mergeObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Converts a managed object into a JSON representation. - @param managedObject The managed object to use for JSON serialization. - + @param object The managed object to use for JSON serialization. + @return A JSON dictionary. */ -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject; ++ (NSDictionary *)JSONDictionaryFromObject:(NSManagedObject *)object; /** Converts an array of managed objects into a JSON representation. - @param managedObjects The array of managed objects to use for JSON serialization. + @param objects The array of managed objects to use for JSON serialization. @return A JSON array. */ -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects; ++ (NSArray *)JSONArrayFromObjects:(NSManagedObject *)objects; + +@end + +@interface GRTJSONSerialization (Deprecated) + ++ (nullable id)insertObjectForEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectWithEntityName:fromJSONDictionary:inContext:error:"))); + ++ (nullable NSArray *)insertObjectsForEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectsWithEntityName:fromJSONArray:inContext:error:"))); + ++ (nullable id)mergeObjectForEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectWithEntityName:fromJSONDictionary:inContext:error:"))); + ++ (nullable NSArray *)mergeObjectsForEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectsWithEntityName:fromJSONArray:inContext:error:"))); + ++ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject __attribute__((deprecated("Replaced by -JSONDictionaryFromObject:"))); + ++ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects __attribute__((deprecated("Replaced by -JSONArrayFromManagedObjects:"))); @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTJSONSerialization.m b/Groot/GRTJSONSerialization.m index add6d32..c884558 100644 --- a/Groot/GRTJSONSerialization.m +++ b/Groot/GRTJSONSerialization.m @@ -1,6 +1,6 @@ // GRTJSONSerialization.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,395 +22,47 @@ #import "GRTJSONSerialization.h" -#import "NSPropertyDescription+Groot.h" -#import "NSAttributeDescription+Groot.h" -#import "NSEntityDescription+Groot.h" -#import "NSDictionary+Groot.h" - -NSString * const GRTJSONSerializationErrorDomain = @"GRTJSONSerializationErrorDomain"; -const NSInteger GRTJSONSerializationErrorInvalidJSONObject = 0xcaca; +NS_ASSUME_NONNULL_BEGIN @implementation GRTJSONSerialization -+ (id)insertObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(JSONDictionary); - - return [[self insertObjectsForEntityName:entityName fromJSONArray:@[JSONDictionary] inManagedObjectContext:context error:error] firstObject]; -} - -+ (NSArray *)insertObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(entityName); - NSParameterAssert(JSONArray); - NSParameterAssert(context); - - NSError * __block tmpError = nil; - NSMutableArray * __block managedObjects = [NSMutableArray arrayWithCapacity:JSONArray.count]; - - if (JSONArray.count == 0) { - // Return early and avoid any processing in the context queue - return managedObjects; - } - - [context performBlockAndWait:^{ - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - if (![dictionary isKindOfClass:NSDictionary.class]) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize value %@. Expected a JSON dictionary.", @""), dictionary]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - tmpError = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - - break; - } - - NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if ([property isKindOfClass:NSAttributeDescription.class]) { - *stop = ![self serializeAttribute:(NSAttributeDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:NO error:&tmpError]; - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - *stop = ![self serializeRelationship:(NSRelationshipDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:NO error:&tmpError]; - } - }]; - - if (tmpError == nil) { - [managedObjects addObject:managedObject]; - } else { - [context deleteObject:managedObject]; - break; - } - } - }]; - - if (error != nil) { - *error = tmpError; - } - - return managedObjects; -} - -+ (id)mergeObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(JSONDictionary); - - return [[self mergeObjectsForEntityName:entityName fromJSONArray:@[JSONDictionary] inManagedObjectContext:context error:error] firstObject]; -} - -+ (NSArray *)mergeObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(entityName); - NSParameterAssert(JSONArray); - NSParameterAssert(context); - - NSError * __block tmpError = nil; - NSMutableArray * __block managedObjects = [NSMutableArray arrayWithCapacity:JSONArray.count]; - - if (JSONArray.count == 0) { - // Return early and avoid any processing in the context queue - return managedObjects; - } - - [context performBlockAndWait:^{ - NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:context]; - - NSAttributeDescription *identityAttribute = [entity grt_identityAttribute]; - NSAssert(identityAttribute != nil, @"An identity attribute must be specified in order to merge objects"); - NSAssert([identityAttribute grt_JSONKeyPath] != nil, @"The identity attribute must have an valid JSON key path"); - - NSMutableArray *identifiers = [NSMutableArray arrayWithCapacity:JSONArray.count]; - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - if (![dictionary isKindOfClass:NSDictionary.class]) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize value %@. Expected a JSON dictionary.", @""), dictionary]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - tmpError = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - return; - } - - id identifier = [dictionary grt_valueForAttribute:identityAttribute]; - if (identifier != nil) [identifiers addObject:identifier]; - } - - NSDictionary *existingObjects = [self fetchObjectsForEntity:entity withIdentifiers:identifiers inManagedObjectContext:context error:&tmpError]; - - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - NSManagedObject *managedObject = nil; - id identifier = [dictionary grt_valueForAttribute:identityAttribute]; - - if (identifier) { - managedObject = existingObjects[identifier]; - } - - if (!managedObject) { - managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; - } - - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if ([property isKindOfClass:NSAttributeDescription.class]) { - *stop = ![self serializeAttribute:(NSAttributeDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:YES error:&tmpError]; - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - *stop = ![self serializeRelationship:(NSRelationshipDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:YES error:&tmpError]; - } - }]; - - if (tmpError == nil) { - [managedObjects addObject:managedObject]; - } else { - [context deleteObject:managedObject]; - break; - } - } - }]; - - if (error != nil) { - *error = tmpError; - } - - return managedObjects; -} - -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject { - // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships - NSMutableSet *processingRelationships = [NSMutableSet set]; - return [self JSONDictionaryFromManagedObject:managedObject processingRelationships:processingRelationships]; -} - -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects { - // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships - NSMutableSet *processingRelationships = [NSMutableSet set]; - return [self JSONArrayFromManagedObjects:managedObjects processingRelationships:processingRelationships]; -} - -#pragma mark - Private - -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject processingRelationships:(NSMutableSet *)processingRelationships { - NSMutableDictionary * __block JSONDictionary = nil; - NSManagedObjectContext *context = managedObject.managedObjectContext; - - if (!managedObject) { - return nil; - } - - [context performBlockAndWait:^{ - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - JSONDictionary = [NSMutableDictionary dictionaryWithCapacity:propertiesByName.count]; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - NSString *JSONKeyPath = [property grt_JSONKeyPath]; - - if (JSONKeyPath == nil) { - return; - } - - id value = [managedObject valueForKey:name]; - - if ([property isKindOfClass:NSAttributeDescription.class]) { - NSAttributeDescription *attribute = (NSAttributeDescription *)property; - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - - if (transformer != nil && [transformer.class allowsReverseTransformation]) { - value = [transformer reverseTransformedValue:value]; - } - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; - - if ([processingRelationships containsObject:relationship.inverseRelationship]) { - // Skip if the inverse relationship is being serialized - return; - } - - [processingRelationships addObject:relationship]; - - if ([relationship isToMany]) { - NSArray *objects = [value isKindOfClass:NSOrderedSet.class] ? [value array] : [value allObjects]; - value = [self JSONArrayFromManagedObjects:objects processingRelationships:processingRelationships]; - } else { - value = [self JSONDictionaryFromManagedObject:value processingRelationships:processingRelationships]; - } - } - - if (value == nil) { - value = NSNull.null; - } - - NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."]; - - if (components.count > 1) { - // Create a dictionary for each key path component - id obj = JSONDictionary; - for (NSString *component in components) { - if ([obj valueForKey:component] == nil) { - [obj setValue:[NSMutableDictionary dictionary] forKey:component]; - } - - obj = [obj valueForKey:component]; - } - } - - [JSONDictionary setValue:value forKeyPath:JSONKeyPath]; - }]; - }]; - - return JSONDictionary; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONData:(NSData *)data + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)outError +{ + // TODO: implement + return nil; } -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects processingRelationships:(NSMutableSet *)processingRelationships { - NSMutableArray *JSONArray = [NSMutableArray arrayWithCapacity:managedObjects.count]; - - for (NSManagedObject *managedObject in managedObjects) { - NSDictionary *JSONDictionary = [self JSONDictionaryFromManagedObject:managedObject processingRelationships:processingRelationships]; - [JSONArray addObject:JSONDictionary]; - } - - return JSONArray; ++ (nullable id)objectWithEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)outError +{ + // TODO: implement + return nil; } -+ (BOOL)serializeAttribute:(NSAttributeDescription *)attribute fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObject:(NSManagedObject *)managedObject merge:(BOOL)merge error:(NSError *__autoreleasing *)error { - NSString *keyPath = [attribute grt_JSONKeyPath]; - - if (keyPath == nil) { - return YES; - } - - id value = [JSONDictionary valueForKeyPath:keyPath]; - - if (merge && value == nil) { - return YES; - } - - if ([value isEqual:NSNull.null]) { - value = nil; - } - - if (value != nil) { - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - if (transformer) { - value = [transformer transformedValue:value]; - } - } - - if ([managedObject validateValue:&value forKey:attribute.name error:error]) { - [managedObject setValue:value forKey:attribute.name]; - return YES; - } - - return NO; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)outError +{ + // TODO: implement + return nil; } -+ (BOOL)serializeRelationship:(NSRelationshipDescription *)relationship fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObject:(NSManagedObject *)managedObject merge:(BOOL)merge error:(NSError *__autoreleasing *)error { - NSString *keyPath = [relationship grt_JSONKeyPath]; - - if (keyPath == nil) { - return YES; - } - - id value = [JSONDictionary valueForKeyPath:keyPath]; - - if (merge && value == nil) { - return YES; - } - - if ([value isEqual:NSNull.null]) { - value = nil; - } - - if (value != nil) { - NSString *entityName = relationship.destinationEntity.name; - NSManagedObjectContext *context = managedObject.managedObjectContext; - NSError *tmpError = nil; - - if ([relationship isToMany]) { - if (![value isKindOfClass:[NSArray class]]) { - if (error) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize '%@' into a to-many relationship. Expected a JSON array.", @""), [relationship grt_JSONKeyPath]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - *error = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - } - - return NO; - } - - NSArray *objects = merge - ? [self mergeObjectsForEntityName:entityName fromJSONArray:value inManagedObjectContext:context error:&tmpError] - : [self insertObjectsForEntityName:entityName fromJSONArray:value inManagedObjectContext:context error:&tmpError]; - - value = [relationship isOrdered] ? [NSOrderedSet orderedSetWithArray:objects] : [NSSet setWithArray:objects]; - } else { - if (![value isKindOfClass:[NSDictionary class]]) { - if (error) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize '%@' into a to-one relationship. Expected a JSON dictionary.", @""), [relationship grt_JSONKeyPath]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - *error = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - } - - return NO; - } - - value = merge - ? [self mergeObjectForEntityName:entityName fromJSONDictionary:value inManagedObjectContext:context error:&tmpError] - : [self insertObjectForEntityName:entityName fromJSONDictionary:value inManagedObjectContext:context error:&tmpError]; - } - - if (tmpError != nil) { - if (error) { - *error = tmpError; - } - return NO; - } - } - - if ([managedObject validateValue:&value forKey:relationship.name error:error]) { - [managedObject setValue:value forKey:relationship.name]; - return YES; - } - - return NO; ++ (NSDictionary *)JSONDictionaryFromObject:(NSManagedObject *)object { + // TODO: implement + return nil; } -+ (NSDictionary *)fetchObjectsForEntity:(NSEntityDescription *)entity withIdentifiers:(NSArray *)identifiers inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSString *identityKey = [[entity grt_identityAttribute] name]; - - NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; - fetchRequest.entity = entity; - fetchRequest.returnsObjectsAsFaults = NO; - fetchRequest.predicate = [NSPredicate predicateWithFormat:@"%K IN %@", identityKey, identifiers]; - - NSArray *objects = [context executeFetchRequest:fetchRequest error:error]; - - if (objects.count > 0) { - NSMutableDictionary *objectsByIdentifier = [NSMutableDictionary dictionaryWithCapacity:objects.count]; - - for (NSManagedObject *object in objects) { - id identifier = [object valueForKey:identityKey]; - objectsByIdentifier[identifier] = object; - } - - return objectsByIdentifier; - } - ++ (NSArray *)JSONArrayFromObjects:(NSManagedObject *)objects { + // TODO: implement return nil; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 90cc947..269a0c7 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -24,6 +24,4 @@ @interface NSEntityDescription (Groot) -- (NSAttributeDescription *)grt_identityAttribute; - @end diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index cc7f816..9305421 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -1,6 +1,6 @@ // NSEntityDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,25 +21,7 @@ // THE SOFTWARE. #import "NSEntityDescription+Groot.h" -#import "GRTConstants.h" @implementation NSEntityDescription (Groot) -- (NSAttributeDescription *)grt_identityAttribute { - - NSString *identityAttribute = nil; - NSEntityDescription *entityDescription = self; - - while (entityDescription && !identityAttribute) { - identityAttribute = entityDescription.userInfo[GRTIdentityAttributeKey]; - entityDescription = entityDescription.superentity; - } - - if (identityAttribute) { - return self.attributesByName[identityAttribute]; - } - - return nil; -} - @end diff --git a/Groot/GRTConstants.m b/Groot/Private/NSManagedObject+Groot.h similarity index 58% rename from Groot/GRTConstants.m rename to Groot/Private/NSManagedObject+Groot.h index 53c56aa..70ec17d 100644 --- a/Groot/GRTConstants.m +++ b/Groot/Private/NSManagedObject+Groot.h @@ -1,6 +1,6 @@ -// GRTConstants.m -// -// Copyright (c) 2014 Guillermo Gonzalez +// NSManagedObject+Groot.h +// +// Copyright (c) 2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -8,10 +8,10 @@ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,6 +20,24 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import "GRTConstants.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSManagedObject (Groot) + +- (void)grt_setAttribute:(NSAttributeDescription *)attribute + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error; + +- (void)grt_setRelationship:(NSRelationshipDescription *)relationship + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error; + +- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships; + +@end -NSString * const GRTIdentityAttributeKey = @"identityAttribute"; +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTConstants.h b/Groot/Private/NSManagedObject+Groot.m similarity index 55% rename from Groot/GRTConstants.h rename to Groot/Private/NSManagedObject+Groot.m index c5f85c5..edd234a 100644 --- a/Groot/GRTConstants.h +++ b/Groot/Private/NSManagedObject+Groot.m @@ -1,6 +1,6 @@ -// GRTConstants.h -// -// Copyright (c) 2014 Guillermo Gonzalez +// NSManagedObject+Groot.m +// +// Copyright (c) 2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -8,10 +8,10 @@ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,6 +20,34 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import +#import "NSManagedObject+Groot.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSManagedObject (Groot) + +- (void)grt_setAttribute:(NSAttributeDescription *)attribute + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error +{ + // TODO: implement +} + +- (void)grt_setRelationship:(NSRelationshipDescription *)relationship + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error +{ + // TODO: implement +} + +- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships +{ + // TODO: implement + return nil; +} + +@end -extern NSString * const GRTIdentityAttributeKey; +NS_ASSUME_NONNULL_END diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index cacfbc3..88184d8 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -104,8 +104,8 @@ - (void)testInsertInvalidJSON { [GRTJSONSerialization insertObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testInsertInvalidToManyRelationship { @@ -123,8 +123,8 @@ - (void)testInsertInvalidToManyRelationship { [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testInsertInvalidToOneRelationship { @@ -139,8 +139,8 @@ - (void)testInsertInvalidToOneRelationship { [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testMergeObject { @@ -264,8 +264,8 @@ - (void)testMergeInvalidJSON { [GRTJSONSerialization mergeObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testMergeInvalidToManyRelationship { @@ -283,8 +283,8 @@ - (void)testMergeInvalidToManyRelationship { [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testMergeInvalidToOneRelationship { @@ -299,8 +299,8 @@ - (void)testMergeInvalidToOneRelationship { [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testMergeWithoutIdentityAttribute { From 523a6fc2ff9f847cdc2811736df50772b8f0f98d Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Tue, 14 Jul 2015 11:05:53 +0200 Subject: [PATCH 10/45] Implement serialization API --- Groot/GRTError.h | 1 + Groot/GRTJSONSerialization.h | 2 +- Groot/GRTJSONSerialization.m | 78 +++++++++++++++++++---- Groot/Private/NSEntityDescription+Groot.h | 22 ++++++- Groot/Private/NSEntityDescription+Groot.m | 41 ++++++++++++ Groot/Private/NSManagedObject+Groot.m | 4 +- 6 files changed, 130 insertions(+), 18 deletions(-) diff --git a/Groot/GRTError.h b/Groot/GRTError.h index b7f78f1..673e17d 100644 --- a/Groot/GRTError.h +++ b/Groot/GRTError.h @@ -25,6 +25,7 @@ extern NSString * const GRTErrorDomain; typedef NS_ENUM(NSInteger, GRTError) { + GRTErrorEntityNotFound, GRTErrorInvalidJSONObject, GRTErrorIdentityNotFound }; diff --git a/Groot/GRTJSONSerialization.h b/Groot/GRTJSONSerialization.h index b1dccf2..fc1d8e9 100644 --- a/Groot/GRTJSONSerialization.h +++ b/Groot/GRTJSONSerialization.h @@ -94,7 +94,7 @@ NS_ASSUME_NONNULL_BEGIN @return A JSON array. */ -+ (NSArray *)JSONArrayFromObjects:(NSManagedObject *)objects; ++ (NSArray *)JSONArrayFromObjects:(NSArray *)objects; @end diff --git a/Groot/GRTJSONSerialization.m b/Groot/GRTJSONSerialization.m index c884558..417e1cf 100644 --- a/Groot/GRTJSONSerialization.m +++ b/Groot/GRTJSONSerialization.m @@ -22,6 +22,9 @@ #import "GRTJSONSerialization.h" +#import "NSEntityDescription+Groot.h" +#import "NSManagedObject+Groot.h" + NS_ASSUME_NONNULL_BEGIN @implementation GRTJSONSerialization @@ -29,38 +32,85 @@ @implementation GRTJSONSerialization + (nullable NSArray *)objectsWithEntityName:(NSString *)entityName fromJSONData:(NSData *)data inContext:(NSManagedObjectContext *)context - error:(NSError * __nullable * __nullable)outError + error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement - return nil; + NSError *error = nil; + id parsedJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; + } + + NSArray *array = nil; + if ([parsedJSON isKindOfClass:[NSDictionary class]]) { + array = @[parsedJSON]; + } else if ([parsedJSON isKindOfClass:[NSArray class]]) { + array = parsedJSON; + } + + NSAssert(array != nil, @"Invalid JSON. The top level object must be an NSArray or an NSDictionary"); + + return [self objectsWithEntityName:entityName fromJSONArray:array inContext:context error:outError]; } + (nullable id)objectWithEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inContext:(NSManagedObjectContext *)context - error:(NSError * __nullable * __nullable)outError + error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement - return nil; + NSError *error = nil; + NSEntityDescription *entity = [NSEntityDescription grt_entityForName:entityName inContext:context error:&error]; + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; + } + + BOOL mergeChanges = [entity grt_hasIdentity]; + + return [entity grt_importJSONDictionary:JSONDictionary + inContext:context + mergeChanges:mergeChanges + error:outError]; } + (nullable NSArray *)objectsWithEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inContext:(NSManagedObjectContext *)context - error:(NSError * __nullable * __nullable)outError + error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement - return nil; + NSError *error = nil; + NSEntityDescription *entity = [NSEntityDescription grt_entityForName:entityName inContext:context error:&error]; + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; + } + + BOOL mergeChanges = [entity grt_hasIdentity]; + + return [entity grt_importJSONArray:JSONArray + inContext:context + mergeChanges:mergeChanges + error:outError]; } + (NSDictionary *)JSONDictionaryFromObject:(NSManagedObject *)object { - // TODO: implement - return nil; + // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships + NSMutableSet *relationships = [NSMutableSet set]; + return [object grt_JSONDictionarySerializingRelationships:relationships]; } -+ (NSArray *)JSONArrayFromObjects:(NSManagedObject *)objects { - // TODO: implement - return nil; ++ (NSArray *)JSONArrayFromObjects:(NSArray *)objects { + NSMutableArray *array = [NSMutableArray arrayWithCapacity:objects.count]; + + for (NSManagedObject *object in objects) { + NSDictionary *dictionary = [self JSONDictionaryFromObject:object]; + [array addObject:dictionary]; + } + + return array; } @end diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 269a0c7..29aa97a 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -1,6 +1,6 @@ // NSEntityDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,26 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSEntityDescription (Groot) ++ (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; + +- (BOOL)grt_hasIdentity; + +- (nullable id)grt_importJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error; + +- (nullable NSArray *)grt_importJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error; + @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index 9305421..59ab544 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -21,7 +21,48 @@ // THE SOFTWARE. #import "NSEntityDescription+Groot.h" +#import "GRTError.h" + +NS_ASSUME_NONNULL_BEGIN @implementation NSEntityDescription (Groot) ++ (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)error +{ + NSEntityDescription *entity = [self entityForName:entityName inManagedObjectContext:context]; + + if (entity == nil && error != nil) { + *error = [NSError errorWithDomain:GRTErrorDomain code:GRTErrorEntityNotFound userInfo:nil]; + } + + return entity; +} + +- (BOOL)grt_hasIdentity { + // TODO: implement + return NO; +} + +- (nullable id)grt_importJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)error +{ + // TODO: implement + return nil; +} + +- (nullable NSArray *)grt_importJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)error +{ + // TODO: implement + return nil; +} + @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index edd234a..e0a2eb8 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -29,7 +29,7 @@ @implementation NSManagedObject (Groot) - (void)grt_setAttribute:(NSAttributeDescription *)attribute fromJSONDictionary:(NSDictionary *)dictionary mergeChanges:(BOOL)mergeChanges - error:(NSError * __nullable * __nullable)error + error:(NSError *__autoreleasing __nullable * __nullable)outError { // TODO: implement } @@ -37,7 +37,7 @@ - (void)grt_setAttribute:(NSAttributeDescription *)attribute - (void)grt_setRelationship:(NSRelationshipDescription *)relationship fromJSONDictionary:(NSDictionary *)dictionary mergeChanges:(BOOL)mergeChanges - error:(NSError * __nullable * __nullable)error + error:(NSError *__autoreleasing __nullable * __nullable)outError { // TODO: implement } From 2a6afd87cc5403a1f0a82b410a9778dcb98f0890 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Tue, 14 Jul 2015 12:46:42 +0200 Subject: [PATCH 11/45] Implement NSManagedObject helpers --- Groot/Private/NSManagedObject+Groot.m | 167 +++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 6 deletions(-) diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index e0a2eb8..aab6b80 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -22,6 +22,12 @@ #import "NSManagedObject+Groot.h" +#import "GRTError.h" + +#import "NSPropertyDescription+Groot.h" +#import "NSAttributeDescription+Groot.h" +#import "NSEntityDescription+Groot.h" + NS_ASSUME_NONNULL_BEGIN @implementation NSManagedObject (Groot) @@ -31,7 +37,29 @@ - (void)grt_setAttribute:(NSAttributeDescription *)attribute mergeChanges:(BOOL)mergeChanges error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement + id value = nil; + id rawValue = [attribute grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil) { + if (rawValue != [NSNull null]) { + NSValueTransformer *transformer = [attribute grt_JSONTransformer]; + if (transformer) { + value = [transformer transformedValue:rawValue]; + } else { + value = rawValue; + } + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:attribute.name]; + [self validateValue:&value forKey:attribute.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:attribute.name error:outError]) { + [self setValue:value forKey:attribute.name]; + } } - (void)grt_setRelationship:(NSRelationshipDescription *)relationship @@ -39,13 +67,140 @@ - (void)grt_setRelationship:(NSRelationshipDescription *)relationship mergeChanges:(BOOL)mergeChanges error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement + id value = nil; + id rawValue = [relationship grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil) { + NSError *error = nil; + NSEntityDescription *destinationEntity = relationship.destinationEntity; + + if ([rawValue isKindOfClass:[NSDictionary class]] && !relationship.toMany) { + NSManagedObject *managedObject = [destinationEntity grt_importJSONDictionary:rawValue + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error]; + if (managedObject != nil) { + value = managedObject; + } + } + else if ([rawValue isKindOfClass:[NSArray class]] && relationship.toMany) { + NSArray *managedObjects = [destinationEntity grt_importJSONArray:rawValue + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error]; + if (managedObjects != nil) { + value = relationship.ordered ? [NSOrderedSet orderedSetWithArray:managedObjects] : [NSSet setWithArray:managedObjects]; + } + } + else if (rawValue == [NSNull null]) { + // Nothing to do + } + else { + NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); + NSString *message = [NSString stringWithFormat:format, [relationship grt_JSONKeyPath], relationship.entity.name, relationship.name]; + error = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorInvalidJSONObject + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + if (error != nil) { + if (outError != nil) *outError = error; + return; + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:relationship.name]; + [self validateValue:&value forKey:relationship.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:relationship.name error:outError]) { + [self setValue:value forKey:relationship.name]; + } } -- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships -{ - // TODO: implement - return nil; +- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + NSManagedObjectContext *context = self.managedObjectContext; + NSDictionary *propertiesByName = self.entity.propertiesByName; + + [context performBlockAndWait:^{ + for (NSString *name in propertiesByName) { + NSPropertyDescription *property = propertiesByName[name]; + + if (![property grt_JSONSerializable]) { + continue; + } + + NSString *keyPath = [property grt_JSONKeyPath]; + id value = [self valueForKey:name]; + + if (value == nil) { + value = [NSNull null]; + } else { + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + NSValueTransformer *transformer = [attribute grt_JSONTransformer]; + + if (transformer && [[transformer class] allowsReverseTransformation]) { + value = [transformer reverseTransformedValue:value]; + } + } else if ([property isKindOfClass:[NSRelationshipDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + NSRelationshipDescription *inverseRelationship = relationship.inverseRelationship; + + if ([serializingRelationships containsObject:inverseRelationship]) { + // Skip if the inverse relationship is being serialized + continue; + } + + [serializingRelationships addObject:relationship]; + + if (relationship.toMany) { + NSArray *managedObjects = @[]; + if ([value isKindOfClass:[NSOrderedSet class]]) { + NSOrderedSet *set = value; + managedObjects = set.array; + } else if ([value isKindOfClass:[NSSet class]]) { + NSSet *set = value; + managedObjects = set.allObjects; + } + + NSMutableArray *array = [NSMutableArray arrayWithCapacity:managedObjects.count]; + for (NSManagedObject *managedObject in managedObjects) { + NSDictionary *dictionary = [managedObject grt_JSONDictionarySerializingRelationships:serializingRelationships]; + [array addObject:dictionary]; + } + value = array; + } else { + NSManagedObject *managedObject = value; + value = [managedObject grt_JSONDictionarySerializingRelationships:serializingRelationships]; + } + } + } + + NSMutableArray *components = [[keyPath componentsSeparatedByString:@"."] mutableCopy]; + [components removeLastObject]; + + if (components.count > 0) { + // Create a dictionary for each key path component + NSMutableDictionary *tmpDictionary = dictionary; + for (NSString *component in components) { + if (tmpDictionary[component] == nil) { + tmpDictionary[component] = [NSMutableDictionary dictionary]; + } + + tmpDictionary = tmpDictionary[component]; + } + } + + [dictionary setValue:value forKeyPath:keyPath]; + } + }]; + + return [dictionary copy]; } @end From 743d55b29624641873fa0bb73186160d07b737da Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Tue, 14 Jul 2015 12:48:43 +0200 Subject: [PATCH 12/45] Fix typo --- Groot/GRTJSONSerialization.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Groot/GRTJSONSerialization.h b/Groot/GRTJSONSerialization.h index fc1d8e9..d666e65 100644 --- a/Groot/GRTJSONSerialization.h +++ b/Groot/GRTJSONSerialization.h @@ -122,7 +122,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject __attribute__((deprecated("Replaced by -JSONDictionaryFromObject:"))); -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects __attribute__((deprecated("Replaced by -JSONArrayFromManagedObjects:"))); ++ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects __attribute__((deprecated("Replaced by -JSONArrayFromObjects:"))); @end From 1b1ed440291b7f5245db4b128b2473722b0dbcec Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 15 Jul 2015 10:54:53 +0200 Subject: [PATCH 13/45] Implement NSEntityDescription helpers --- Groot/Private/NSEntityDescription+Groot.h | 4 +- Groot/Private/NSEntityDescription+Groot.m | 148 ++++++++++++++++++++-- 2 files changed, 141 insertions(+), 11 deletions(-) diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 29aa97a..7a03a0a 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -32,12 +32,12 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)grt_hasIdentity; -- (nullable id)grt_importJSONDictionary:(NSDictionary *)JSONDictionary +- (nullable id)grt_importJSONDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges error:(NSError * __nullable * __nullable)error; -- (nullable NSArray *)grt_importJSONArray:(NSArray *)JSONArray +- (nullable NSArray *)grt_importJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges error:(NSError * __nullable * __nullable)error; diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index 59ab544..f58369c 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -21,6 +21,10 @@ // THE SOFTWARE. #import "NSEntityDescription+Groot.h" +#import "NSPropertyDescription+Groot.h" +#import "NSAttributeDescription+Groot.h" +#import "NSManagedObject+Groot.h" + #import "GRTError.h" NS_ASSUME_NONNULL_BEGIN @@ -41,25 +45,151 @@ + (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName } - (BOOL)grt_hasIdentity { - // TODO: implement - return NO; + return [self grt_identityAttribute] != nil; } -- (nullable id)grt_importJSONDictionary:(NSDictionary *)JSONDictionary +- (nullable id)grt_importJSONDictionary:(NSDictionary *)dictionary inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)error + error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement - return nil; + NSArray *managedObjects = [self grt_importJSONArray:@[dictionary] inContext:context mergeChanges:mergeChanges error:outError]; + return managedObjects.firstObject; } -- (nullable NSArray *)grt_importJSONArray:(NSArray *)JSONArray +- (nullable NSArray *)grt_importJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)error + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSMutableArray * __block managedObjects = [NSMutableArray array]; + NSError * __block error = nil; + + if (array.count == 0) { + // Return early and avoid further processing + return managedObjects; + } + + [context performBlockAndWait:^{ + NSDictionary *existingObjects = nil; + + if (mergeChanges) { + existingObjects = [self grt_existingObjectsWithJSONArray:array inContext:context error:&error]; + if (error != nil) return; // exit the block + } + + for (NSDictionary *dictionary in array) { + NSManagedObject *managedObject = nil; + + if (mergeChanges) { + id identifier = [self grt_identifierInJSONDictionary:dictionary]; + if (identifier != nil) { + managedObject = existingObjects[identifier]; + } + } + + if (managedObject == nil) { + managedObject = [[self class] insertNewObjectForEntityForName:self.name inManagedObjectContext:context]; + } + + [self.propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { + if (![property grt_JSONSerializable]) { + return; // continue + } + + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + [managedObject grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } else if ([property isKindOfClass:[NSPropertyDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + [managedObject grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } + + *stop = (error != nil); // break on error + }]; + + if (error != nil) { + [context deleteObject:managedObject]; + return; // exit the block + } + + [managedObjects addObject:managedObject]; + } + }]; + + if (error != nil) { + if (outError) *outError = error; + return nil; + } + + return managedObjects; +} + +#pragma mark - Private + +- (nullable NSAttributeDescription *)grt_identityAttribute { + NSString *attributeName = nil; + NSEntityDescription *entity = self; + + while (entity != nil && attributeName == nil) { + attributeName = entity.userInfo[@"identityAttribute"]; + entity = [entity superentity]; + } + + if (attributeName != nil) { + return self.attributesByName[attributeName]; + } + + return nil; +} + +- (nullable id)grt_identifierInJSONDictionary:(NSDictionary *)dictionary { + NSAttributeDescription *identityAttribute = [self grt_identityAttribute]; + if (identityAttribute != nil) { + return [identityAttribute grt_valueInJSONDictionary:dictionary]; + } + + return nil; +} + +- (nullable NSDictionary *)grt_existingObjectsWithJSONArray:(NSArray *)array + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)outError { - // TODO: implement + NSAttributeDescription *attribute = [self grt_identityAttribute]; + + if (attribute == nil) { + if (outError) { + NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); + NSString *message = [NSString stringWithFormat:format, self.name]; + *outError = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorIdentityNotFound + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + return nil; + } + + NSArray *identifiers = [attribute grt_valuesInJSONArray:array]; + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = self; + fetchRequest.returnsObjectsAsFaults = NO; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"%K IN %@", attribute.name, identifiers]; + + NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:outError]; + + if (fetchedObjects != nil) { + NSMutableDictionary *objects = [NSMutableDictionary dictionaryWithCapacity:fetchedObjects.count]; + + for (NSManagedObject *object in fetchedObjects) { + id identifier = [object valueForKey:attribute.name]; + if (identifier != nil) { + objects[identifier] = object; + } + } + return objects; + } + return nil; } From b71f65f0bab0866b69d0e8fa597cea9cdcb52265 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 15 Jul 2015 14:34:38 +0200 Subject: [PATCH 14/45] Update tests with new serialization API --- Groot.xcodeproj/project.pbxproj | 30 +- Groot/Groot.h | 2 +- Groot/Private/NSEntityDescription+Groot.m | 5 +- Groot/Private/NSManagedObject+Groot.m | 2 +- GrootTests/GRTJSONSerializationTests.m | 278 ++++++------------ GrootTests/JSON/characters.json | 12 + GrootTests/JSON/characters_update.json | 15 + .../Model.xcdatamodel/contents | 46 ++- GrootTests/NSData+Resource.h | 19 ++ GrootTests/NSData+Resource.m | 26 ++ 10 files changed, 233 insertions(+), 202 deletions(-) create mode 100755 GrootTests/JSON/characters.json create mode 100755 GrootTests/JSON/characters_update.json create mode 100644 GrootTests/NSData+Resource.h create mode 100644 GrootTests/NSData+Resource.m diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 525956b..d048c63 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + B42A1D931B56600000309637 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; + B42A1D941B56600000309637 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; + B42A1D971B56614600309637 /* NSData+Resource.m in Sources */ = {isa = PBXBuildFile; fileRef = B42A1D961B56614600309637 /* NSData+Resource.m */; }; B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; B475B4101B53FCE1001F29FE /* NSManagedObject+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */; }; @@ -46,6 +49,10 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B42A1D911B56600000309637 /* characters.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters.json; sourceTree = ""; }; + B42A1D921B56600000309637 /* characters_update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters_update.json; sourceTree = ""; }; + B42A1D951B56614600309637 /* NSData+Resource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Resource.h"; sourceTree = ""; }; + B42A1D961B56614600309637 /* NSData+Resource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Resource.m"; sourceTree = ""; }; B46200C01B4F07E4003B3B69 /* GRTError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTError.h; sourceTree = ""; }; B46200C11B4F07E4003B3B69 /* GRTError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTError.m; sourceTree = ""; }; B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+Groot.h"; sourceTree = ""; }; @@ -97,6 +104,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B42A1D901B56600000309637 /* JSON */ = { + isa = PBXGroup; + children = ( + B42A1D911B56600000309637 /* characters.json */, + B42A1D921B56600000309637 /* characters_update.json */, + ); + path = JSON; + sourceTree = ""; + }; B4DC1AE21AA9CA5E00F67403 = { isa = PBXGroup; children = ( @@ -145,12 +161,10 @@ B4DC1AFB1AA9CA5E00F67403 /* GrootTests */ = { isa = PBXGroup; children = ( + B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */, B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */, B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */, - B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, - B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, - B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */, - B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */, + B42A1D901B56600000309637 /* JSON */, B4DC1AFC1AA9CA5E00F67403 /* Supporting Files */, ); path = GrootTests; @@ -159,6 +173,11 @@ B4DC1AFC1AA9CA5E00F67403 /* Supporting Files */ = { isa = PBXGroup; children = ( + B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, + B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, + B42A1D951B56614600309637 /* NSData+Resource.h */, + B42A1D961B56614600309637 /* NSData+Resource.m */, + B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */, B4DC1AFD1AA9CA5E00F67403 /* Info.plist */, ); name = "Supporting Files"; @@ -287,6 +306,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B42A1D941B56600000309637 /* characters_update.json in Resources */, + B42A1D931B56600000309637 /* characters.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -316,6 +337,7 @@ files = ( B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */, B4DC1B2F1AA9CBAD00F67403 /* GRTJSONSerializationTests.m in Sources */, + B42A1D971B56614600309637 /* NSData+Resource.m in Sources */, B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */, B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */, B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */, diff --git a/Groot/Groot.h b/Groot/Groot.h index a56d445..c9a43c2 100644 --- a/Groot/Groot.h +++ b/Groot/Groot.h @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import +#import //! Project version number for Groot. FOUNDATION_EXPORT double GrootVersionNumber; diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index f58369c..c9d803a 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -78,7 +78,10 @@ - (nullable NSArray *)grt_importJSONArray:(NSArray *)array if (error != nil) return; // exit the block } - for (NSDictionary *dictionary in array) { + for (id obj in array) { + if (![obj isKindOfClass:[NSDictionary class]]) continue; + + NSDictionary *dictionary = obj; NSManagedObject *managedObject = nil; if (mergeChanges) { diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index aab6b80..30edf60 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -97,7 +97,7 @@ - (void)grt_setRelationship:(NSRelationshipDescription *)relationship } else { NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); - NSString *message = [NSString stringWithFormat:format, [relationship grt_JSONKeyPath], relationship.entity.name, relationship.name]; + NSString *message = [NSString stringWithFormat:format, rawValue, relationship.entity.name, relationship.name]; error = [NSError errorWithDomain:GRTErrorDomain code:GRTErrorInvalidJSONObject userInfo:@{ NSLocalizedDescriptionKey: message }]; diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 88184d8..1ac62ef 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -9,8 +9,8 @@ #import #import +#import "NSData+Resource.h" #import "GRTModels.h" -#import "NSEntityDescription+Groot.h" @interface GRTJSONSerializationTests : XCTestCase @@ -24,19 +24,13 @@ @implementation GRTJSONSerializationTests - (void)setUp { [super setUp]; - NSBundle *bundle = [NSBundle bundleForClass:[self class]]; - NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; + self.store = [[GRTManagedStore alloc] initWithModel:[NSManagedObjectModel grt_testModel] error:nil]; + XCTAssertNotNil(self.store); - self.store = [[GRTManagedStore alloc] initWithModel:model error:nil]; - self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; - self.context.persistentStoreCoordinator = self.store.persistentStoreCoordinator; + self.context = [self.store contextWithConcurrencyType:NSMainQueueConcurrencyType]; - [NSValueTransformer grt_setValueTransformerWithName:@"GRTTestTransformer" transformBlock:^id(NSString *value) { - if (value) { - return @([value integerValue]); - } - - return nil; + [NSValueTransformer grt_setValueTransformerWithName:@"GrootTests.Transformer" transformBlock:^id(NSString *value) { + return @([value integerValue]); } reverseTransformBlock:^id(NSNumber *value) { return [value stringValue]; }]; @@ -44,7 +38,9 @@ - (void)setUp { - (void)tearDown { self.store = nil; - [NSValueTransformer setValueTransformer:nil forName:@"GRTTestTransformer"]; + self.context = nil; + + [NSValueTransformer setValueTransformer:nil forName:@"GrootTests.Transformer"]; [super tearDown]; } @@ -72,72 +68,64 @@ - (void)testInsertObject { }; NSError *error = nil; - GRTCharacter *batman = [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:batmanJSON inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; XCTAssertNil(error, @"shouldn't return an error"); - XCTAssertEqualObjects(@"Character", batman.entity.name, @"should serialize to the right entity"); - - XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@1699, batman.identifier, @"should serialize attributes"); + XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@"Bruce Wayne", batman.realName, @"should serialize attributes"); - GRTPower *power = batman.powers[0]; + XCTAssertEqual(2U, batman.powers.count, "should serialize to-many relationships"); - XCTAssertEqualObjects(@"Power", power.entity.name, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@4, power.identifier, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@"Agility", power.name, @"should serialize to-many relationships"); + GRTPower *agility = batman.powers[0]; - power = batman.powers[1]; + XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@9, power.identifier, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@"Insanely Rich", power.name, @"should serialize to-many relationships"); + GRTPower *wealth = batman.powers[1]; - XCTAssertEqualObjects(@"Publisher", batman.publisher.entity.name, @"should serialize to-one relationships"); - XCTAssertEqualObjects(@10, batman.publisher.identifier, @"should serialize to-one relationships"); - XCTAssertEqualObjects(@"DC Comics", batman.publisher.name, @"should serialize to-one relationships"); -} - -- (void)testInsertInvalidJSON { - NSArray *invalidJSON = @[@1]; + XCTAssertEqualObjects(@9, wealth.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Insanely Rich", wealth.name, @"should serialize to-many relationships"); - NSError *error = nil; - [GRTJSONSerialization insertObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; + GRTPublisher *publisher = batman.publisher; - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertNotNil(publisher, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@10, publisher.identifier, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@"DC Comics", publisher.name, @"should serialize to-one relationships"); } -- (void)testInsertInvalidToManyRelationship { +- (void)testInsertInvalidToOneRelationship { NSDictionary *invalidBatman = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"powers": @{ // This should be a JSON array - @"id": @"4", - @"name": @"Agility" - } + @"publisher": @"DC" // This should be a JSON dictionary }; NSError *error = nil; - [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:invalidBatman inContext:self.context error:&error]; + XCTAssertNil(batman, @"should return nil on error"); XCTAssertNotNil(error, @"should return an error"); XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } -- (void)testInsertInvalidToOneRelationship { +- (void)testInsertInvalidToManyRelationship { NSDictionary *invalidBatman = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"publisher": @"DC" // This should be a JSON dictionary + @"powers": @{ // This should be a JSON array + @"id": @"4", + @"name": @"Agility" + } }; NSError *error = nil; - [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:invalidBatman inContext:self.context error:&error]; + XCTAssertNil(batman, @"should return nil on error"); XCTAssertNotNil(error, @"should return an error"); XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); @@ -147,7 +135,7 @@ - (void)testMergeObject { NSDictionary *batmanJSON = @{ @"id": @"1699", @"name": @"Batman", - @"real_name": @"Bruce Wayne", + @"real_name": @"Guille Gonzalez", @"powers": @[ @{ @"id": @"4", @@ -166,13 +154,14 @@ - (void)testMergeObject { }; NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batmanJSON inManagedObjectContext:self.context error:&error]; - XCTAssertNil(error, @"shouldn't return an error"); + + [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; + XCTAssertNil(error); NSArray *updateJSON = @[ @{ @"id": @"1699", - @"real_name": NSNull.null, // Should reset Batman real name + @"real_name": @"Bruce Wayne", // Should update real name @"publisher" : @{ @"id": @"10", @"name": @"DC Comics" // Should update the publisher name @@ -195,30 +184,28 @@ - (void)testMergeObject { } ]; - [GRTJSONSerialization mergeObjectsForEntityName:@"Character" fromJSONArray:updateJSON inManagedObjectContext:self.context error:&error]; - XCTAssertNil(error, @"shouldn't return an error"); + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:updateJSON inContext:self.context error:&error]; + XCTAssertNil(error); NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier" ascending:YES]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Character"]; fetchRequest.sortDescriptors = @[sortDescriptor]; NSArray *characters = [self.context executeFetchRequest:fetchRequest error:NULL]; - XCTAssertEqual(2U, characters.count, @"there should be 2 characters"); GRTCharacter *ironMan = characters[0]; - XCTAssertEqualObjects(@"Character", ironMan.entity.name, @"should serialize to the right entity"); XCTAssertEqualObjects(@"Iron Man", ironMan.name, @"should serialize attributes"); XCTAssertEqualObjects(@1455, ironMan.identifier, @"should serialize attributes"); XCTAssertEqualObjects(@"Tony Stark", ironMan.realName, @"should serialize attributes"); GRTPower *powerSuit = ironMan.powers[0]; - XCTAssertEqualObjects(@"Power", powerSuit.entity.name, @"should serialize to-many relationships"); XCTAssertEqualObjects(@31, powerSuit.identifier, @"should serialize to-many relationships"); XCTAssertEqualObjects(@"Power Suit", powerSuit.name, @"should serialize to-many relationships"); + GRTPower *ironManRich = ironMan.powers[1]; XCTAssertEqualObjects(@9, ironManRich.identifier, @"should serialize to-many relationships"); @@ -226,17 +213,15 @@ - (void)testMergeObject { GRTCharacter *batman = characters[1]; - XCTAssertEqualObjects(@"Character", batman.entity.name, @"should serialize to the right entity"); XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@1699, batman.identifier, @"should serialize attributes"); - XCTAssertNil(batman.realName, @"should update explicit null values"); + XCTAssertEqualObjects(@"Bruce Wayne", batman.realName, @"should serialize attributes"); GRTPower *agility = batman.powers[0]; - XCTAssertEqualObjects(@"Power", agility.entity.name, @"should serialize to-many relationships"); XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); - + GRTPower *batmanRich = batman.powers[1]; XCTAssertEqualObjects(@9, batmanRich.identifier, @"should serialize to-many relationships"); @@ -244,7 +229,6 @@ - (void)testMergeObject { XCTAssertEqualObjects(batmanRich, ironManRich, @"should merge relationships properly"); - XCTAssertEqualObjects(@"Publisher", batman.publisher.entity.name, @"should serialize to-one relationships"); XCTAssertEqualObjects(@10, batman.publisher.identifier, @"should serialize to-one relationships"); XCTAssertEqualObjects(@"DC Comics", batman.publisher.name, @"should serialize to-one relationships"); @@ -257,57 +241,30 @@ - (void)testMergeObject { XCTAssertEqual(1U, publisherCount, @"there should be 1 publisher objects"); } -- (void)testMergeInvalidJSON { - NSArray *invalidJSON = @[@1]; +- (void)testValidationDuringMerge { + // See https://github.com/gonzalezreal/Groot/issues/2 - NSError *error = nil; - [GRTJSONSerialization mergeObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; - - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); -} - -- (void)testMergeInvalidToManyRelationship { - NSDictionary *invalidBatman = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @{ // This should be a JSON array - @"id": @"4", - @"name": @"Agility" - } - }; + NSData *data = [NSData grt_dataWithContentsOfResource:@"characters.json"]; + XCTAssertNotNil(data, @"characters.json not found"); NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; - - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); -} - -- (void)testMergeInvalidToOneRelationship { - NSDictionary *invalidBatman = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"publisher": @"DC" // This should be a JSON dictionary - }; + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONData:data inContext:self.context error:&error]; + XCTAssertNil(error); - NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + NSData *updatedData = [NSData grt_dataWithContentsOfResource:@"characters_update.json"]; + XCTAssertNotNil(updatedData, @"characters_update.json not found"); - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONData:updatedData inContext:self.context error:&error]; + XCTAssertNotNil(error, "should return an error"); + XCTAssertEqualObjects(NSCocoaErrorDomain, error.domain, "should return a validation error"); + XCTAssertEqual(NSValidationMissingMandatoryPropertyError, error.code, "should return a validation error"); } -- (void)testMergeWithoutIdentityAttribute { +- (void)testMissingIdentityAttribute { NSEntityDescription *powerEntity = self.store.managedObjectModel.entitiesByName[@"Power"]; powerEntity.userInfo = @{}; // Remove the identity attribute name from the entity - NSDictionary *batman = @{ + NSDictionary *dictionary = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", @@ -319,32 +276,17 @@ - (void)testMergeWithoutIdentityAttribute { ] }; - XCTAssertThrows([GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batman inManagedObjectContext:self.context error:NULL], @"merge should assert when the identity attribute is not specified"); -} - -- (void)testMergeWithNoIdentityAttributeJSONKeyPath { - NSEntityDescription *powerEntity = self.store.managedObjectModel.entitiesByName[@"Power"]; - NSAttributeDescription *identityAttribute = [powerEntity grt_identityAttribute]; - identityAttribute.userInfo = @{ - @"JSONKeyPath": @"null" - }; + NSError *error = nil; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:dictionary inContext:self.context error:&error]; - NSDictionary *batman = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ - @{ - @"id": @"4", - @"name": @"Agility" - } - ] - }; + XCTAssertNil(batman); + XCTAssertNotNil(error); - XCTAssertThrows([GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batman inManagedObjectContext:self.context error:NULL], @"merge should assert when the identity attribute doesn't have a JSON key path"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain); + XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); } -- (void)testJSONDictionaryFromManagedObject { +- (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; dc.name = @"DC Comics"; @@ -364,13 +306,17 @@ - (void)testJSONDictionaryFromManagedObject { batman.powers = [[NSOrderedSet alloc] initWithArray:@[agility, wealth]]; batman.publisher = dc; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; + GRTCharacter *ironMan = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; + ironMan.name = @"Iron Man"; - NSDictionary *expectedDictionary = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ + NSArray *JSONArray = [GRTJSONSerialization JSONArrayFromObjects:@[batman, ironMan]]; + + NSArray *expectedArray = @[ + @{ + @"id": @"1699", + @"name": @"Batman", + @"real_name": @"Bruce Wayne", + @"powers": @[ @{ @"id": @"4", @"name": @"Agility" @@ -379,49 +325,46 @@ - (void)testJSONDictionaryFromManagedObject { @"id": @"9", @"name": @"Insanely Rich" } - ], - @"publisher": @{ - @"id": @"10", - @"name": @"DC Comics" + ], + @"publisher": @{ + @"id": @"10", + @"name": @"DC Comics" + } + }, + @{ + @"id": NSNull.null, + @"name": @"Iron Man", + @"real_name": NSNull.null, + @"powers": @[], + @"publisher": NSNull.null } - }; - - XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize an initialized managed object to JSON dictionary"); -} - -- (void)testJSONDictionaryFromEmptyManagedObject { - GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; - - NSDictionary *expectedDictionary = @{ - @"id": NSNull.null, - @"name": NSNull.null, - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - }; + ]; - XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize an uninitialized managed object to JSON dictionary"); + XCTAssertEqualObjects(expectedArray, JSONArray, @"should serialize managed objects to JSON array"); } -- (void)testJSONDictionaryFromManagedObjectWithNestedDictionaries { +- (void)testSerializationToJSONWithNestedDictionaries { NSEntityDescription *entity = self.store.managedObjectModel.entitiesByName[@"Character"]; NSAttributeDescription *realNameAttribute = entity.attributesByName[@"realName"]; realNameAttribute.userInfo = @{ - @"JSONKeyPath": @"real_name.name" + @"JSONKeyPath": @"real_name.foo.bar.name" }; GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; batman.realName = @"Bruce Wayne"; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; + NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromObject:batman]; NSDictionary *expectedDictionary = @{ @"id": NSNull.null, @"name": NSNull.null, @"real_name": @{ - @"name": @"Bruce Wayne" + @"foo": @{ + @"bar": @{ + @"name": @"Bruce Wayne" + } + } }, @"powers": @[], @"publisher": NSNull.null @@ -430,33 +373,4 @@ - (void)testJSONDictionaryFromManagedObjectWithNestedDictionaries { XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize attributes with complex JSON key paths"); } -- (void)testJSONArrayFromManagedObjects { - GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - batman.name = @"Batman"; - - GRTCharacter *ironMan = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - ironMan.name = @"Iron Man"; - - NSArray *JSONArray = [GRTJSONSerialization JSONArrayFromManagedObjects:@[batman, ironMan]]; - - NSArray *expectedArray = @[ - @{ - @"id": NSNull.null, - @"name": @"Batman", - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - }, - @{ - @"id": NSNull.null, - @"name": @"Iron Man", - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - } - ]; - - XCTAssertEqualObjects(expectedArray, JSONArray, @"should serialize managed objects to JSON array"); -} - @end diff --git a/GrootTests/JSON/characters.json b/GrootTests/JSON/characters.json new file mode 100755 index 0000000..b38138f --- /dev/null +++ b/GrootTests/JSON/characters.json @@ -0,0 +1,12 @@ +[ + { + "id": "1699", + "name": "Batman", + "real_name": "Guille Gonzalez" + }, + { + "id": "1700", + "name": "Superman", + "real_name": "Franck Letellier" + } +] diff --git a/GrootTests/JSON/characters_update.json b/GrootTests/JSON/characters_update.json new file mode 100755 index 0000000..c6dbcb6 --- /dev/null +++ b/GrootTests/JSON/characters_update.json @@ -0,0 +1,15 @@ +[ + { + "id": "1701", + "name": "Spiderman", + "_comment": "Should return a validation error when merging" + }, + { + "id": "1699", + "real_name": "Bruce Wayne" + }, + { + "id": "1700", + "real_name": "Clark Kent" + } +] diff --git a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents index ada87bd..df5cc66 100644 --- a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,45 +1,65 @@ - + - + - + - - + + + + + + - - + + + + + + + + + + - + - + + + + + + - - + - + + + + + + - diff --git a/GrootTests/NSData+Resource.h b/GrootTests/NSData+Resource.h new file mode 100644 index 0000000..707a539 --- /dev/null +++ b/GrootTests/NSData+Resource.h @@ -0,0 +1,19 @@ +// +// NSData+Resource.h +// Groot +// +// Created by Guillermo Gonzalez on 15/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (Resource) + ++ (nullable instancetype)grt_dataWithContentsOfResource:(NSString *)resource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GrootTests/NSData+Resource.m b/GrootTests/NSData+Resource.m new file mode 100644 index 0000000..2c65ad0 --- /dev/null +++ b/GrootTests/NSData+Resource.m @@ -0,0 +1,26 @@ +// +// NSData+Resource.m +// Groot +// +// Created by Guillermo Gonzalez on 15/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import "NSData+Resource.h" + +@interface Dummy : NSObject +@end + +@implementation Dummy +@end + +@implementation NSData (Resource) + ++ (nullable instancetype)grt_dataWithContentsOfResource:(NSString * __nonnull)resource { + NSBundle *bundle = [NSBundle bundleForClass:[Dummy class]]; + NSString *path = [bundle pathForResource:[resource stringByDeletingPathExtension] ofType:resource.pathExtension]; + + return [self dataWithContentsOfFile:path]; +} + +@end From 4aa6c281fca7a2cded53740345ceb8fe96564782 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Wed, 15 Jul 2015 16:59:04 +0200 Subject: [PATCH 15/45] Add swift-friendly API --- Groot.xcodeproj/project.pbxproj | 4 + Groot/Groot.swift | 164 ++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 Groot/Groot.swift diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index d048c63..ebe3926 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */; }; B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */; }; + B4FD30711B569F1A002392F7 /* Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FD30701B569F1A002392F7 /* Groot.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -83,6 +84,7 @@ B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSValueTransformer+Groot.m"; sourceTree = ""; }; B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSValueTransformer+Groot.swift"; sourceTree = ""; }; B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSValueTransformerTests.swift; sourceTree = ""; }; + B4FD30701B569F1A002392F7 /* Groot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Groot.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -135,6 +137,7 @@ isa = PBXGroup; children = ( B4DC1AF11AA9CA5E00F67403 /* Groot.h */, + B4FD30701B569F1A002392F7 /* Groot.swift */, B46200C01B4F07E4003B3B69 /* GRTError.h */, B46200C11B4F07E4003B3B69 /* GRTError.m */, B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */, @@ -320,6 +323,7 @@ files = ( B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */, B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */, + B4FD30711B569F1A002392F7 /* Groot.swift in Sources */, B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */, B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */, B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */, diff --git a/Groot/Groot.swift b/Groot/Groot.swift new file mode 100644 index 0000000..4ec3035 --- /dev/null +++ b/Groot/Groot.swift @@ -0,0 +1,164 @@ +// Groot.swift +// +// Copyright (c) 2015 Guillermo Gonzalez +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreData + +extension NSManagedObjectContext { + internal func managedObjectModel() -> NSManagedObjectModel { + if let psc = persistentStoreCoordinator { + return psc.managedObjectModel + } + + return parentContext!.managedObjectModel() + } +} + +extension NSManagedObject { + internal class func entityInManagedObjectContext(context: NSManagedObjectContext) -> NSEntityDescription { + let className = NSStringFromClass(self) + let model = context.managedObjectModel() + let entities = model.entities as! [NSEntityDescription] + + for entity in entities { + if entity.managedObjectClassName == className { + return entity + } + } + + assert(false, "Could not locate the entity for \(className).") + return NSEntityDescription() + } +} + +/** + Creates or updates a set of managed objects from JSON data. + + :param: entityName The name of an entity. + :param: fromJSONData A data object containing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsWithEntityName(name: String, fromJSONData data: NSData, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [NSManagedObject]? { + return GRTJSONSerialization.objectsWithEntityName(name, fromJSONData: data, inContext: context, error: outError) as? [NSManagedObject] +} + +/** + Creates or updates a set of managed objects from JSON data. + + :param: fromJSONData A data object containing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsFromJSONData(data: NSData, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [T]? { + let entity = T.entityInManagedObjectContext(context) + return objectsWithEntityName(entity.name!, fromJSONData: data, inContext: context, error: outError) as? [T] +} + +public typealias JSONDictionary = [String: AnyObject] + +/** + Creates or updates a managed object from a JSON dictionary. + + This method converts the specified JSON dictionary into a managed object of a given entity. + + :param: entityName The name of an entity. + :param: fromJSONDictionary A dictionary representing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: A managed object, or `nil` if an error occurs. + */ +public func objectWithEntityName(name: String, fromJSONDictionary dictionary: JSONDictionary, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> NSManagedObject? { + return GRTJSONSerialization.objectWithEntityName(name, fromJSONDictionary: dictionary, inContext: context, error: outError) as? NSManagedObject +} + +/** + Creates or updates a managed object from a JSON dictionary. + + This method converts the specified JSON dictionary into a managed object. + + :param: fromJSONDictionary A dictionary representing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: A managed object, or `nil` if an error occurs. + */ +public func objectFromJSONDictionary(dictionary: JSONDictionary, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> T? { + let entity = T.entityInManagedObjectContext(context) + return objectWithEntityName(entity.name!, fromJSONDictionary: dictionary, inContext: context, error: outError) as? T; +} + +public typealias JSONArray = [AnyObject] + +/** + Creates or updates a set of managed objects from a JSON array. + + :param: entityName The name of an entity. + :param: fromJSONArray An array representing JSON data. + :param: context The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsWithEntityName(name: String, fromJSONArray array: JSONArray, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [NSManagedObject]? { + return GRTJSONSerialization.objectsWithEntityName(name, fromJSONArray: array, inContext: context, error: outError) as? [NSManagedObject] +} + +/** + Creates or updates a set of managed objects from a JSON array. + + :param: fromJSONArray An array representing JSON data. + :param: context The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsFromJSONArray(array: JSONArray, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [T]? { + let entity = T.entityInManagedObjectContext(context) + return objectsWithEntityName(entity.name!, fromJSONArray: array, inContext: context, error: outError) as? [T] +} + +/** + Converts a managed object into a JSON representation. + + :param: object The managed object to use for JSON serialization. + + :return: A JSON dictionary. + */ +public func JSONDictionaryFromObject(object: NSManagedObject) -> JSONDictionary { + return GRTJSONSerialization.JSONDictionaryFromObject(object) as! JSONDictionary; +} + +/** + Converts an array of managed objects into a JSON representation. + + :param: objects The array of managed objects to use for JSON serialization. + + :return: A JSON array. + */ +public func JSONArrayFromObjects(objects: [NSManagedObject]) -> JSONArray { + return GRTJSONSerialization.JSONArrayFromObjects(objects) +} From 6609cbce1349861ff88a97b9bfa08795265ce377 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Thu, 16 Jul 2015 09:07:29 +0200 Subject: [PATCH 16/45] Use a single helper method to import JSON --- Groot/GRTJSONSerialization.m | 8 ++++---- Groot/Private/NSEntityDescription+Groot.h | 5 ----- Groot/Private/NSEntityDescription+Groot.m | 9 --------- Groot/Private/NSManagedObject+Groot.m | 9 +++++---- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/Groot/GRTJSONSerialization.m b/Groot/GRTJSONSerialization.m index 417e1cf..348a487 100644 --- a/Groot/GRTJSONSerialization.m +++ b/Groot/GRTJSONSerialization.m @@ -69,10 +69,10 @@ + (nullable id)objectWithEntityName:(NSString *)entityName BOOL mergeChanges = [entity grt_hasIdentity]; - return [entity grt_importJSONDictionary:JSONDictionary - inContext:context - mergeChanges:mergeChanges - error:outError]; + return [entity grt_importJSONArray:@[JSONDictionary] + inContext:context + mergeChanges:mergeChanges + error:outError].firstObject; } + (nullable NSArray *)objectsWithEntityName:(NSString *)entityName diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 7a03a0a..83dec2a 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -32,11 +32,6 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)grt_hasIdentity; -- (nullable id)grt_importJSONDictionary:(NSDictionary *)dictionary - inContext:(NSManagedObjectContext *)context - mergeChanges:(BOOL)mergeChanges - error:(NSError * __nullable * __nullable)error; - - (nullable NSArray *)grt_importJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index c9d803a..973aee6 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -48,15 +48,6 @@ - (BOOL)grt_hasIdentity { return [self grt_identityAttribute] != nil; } -- (nullable id)grt_importJSONDictionary:(NSDictionary *)dictionary - inContext:(NSManagedObjectContext *)context - mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)outError -{ - NSArray *managedObjects = [self grt_importJSONArray:@[dictionary] inContext:context mergeChanges:mergeChanges error:outError]; - return managedObjects.firstObject; -} - - (nullable NSArray *)grt_importJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context mergeChanges:(BOOL)mergeChanges diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index 30edf60..cc80106 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -75,10 +75,11 @@ - (void)grt_setRelationship:(NSRelationshipDescription *)relationship NSEntityDescription *destinationEntity = relationship.destinationEntity; if ([rawValue isKindOfClass:[NSDictionary class]] && !relationship.toMany) { - NSManagedObject *managedObject = [destinationEntity grt_importJSONDictionary:rawValue - inContext:self.managedObjectContext - mergeChanges:mergeChanges - error:&error]; + NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error].firstObject; + if (managedObject != nil) { value = managedObject; } From 102453d9646c260bc29e8444edcd3132ac6630c8 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 10:38:12 +0200 Subject: [PATCH 17/45] Add tests for identifier serialization --- GrootTests/GRTJSONSerializationTests.m | 72 ++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 1ac62ef..41ef822 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -286,6 +286,78 @@ - (void)testMissingIdentityAttribute { XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); } +- (void)testSerializationFromIdentifiers { + NSArray *charactersJSON = @[@"1699", @"1455"]; + + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + XCTAssertEqual(2U, characters.count); + + GRTCharacter *character = characters[0]; + XCTAssertEqualObjects(@"Character", character.entity.name); + XCTAssertEqualObjects(@1699, character.identifier); + + character = characters[1]; + XCTAssertEqualObjects(@"Character", character.entity.name); + XCTAssertEqualObjects(@1455, character.identifier); +} + +- (void)testRelationshipSerializationFromIdentifiers { + NSDictionary *batmanJSON = @{ + @"name": @"Batman", + @"real_name": @"Bruce Wayne", + @"id": @"1699", + @"powers": @[@"4", NSNull.null, @"9"], + @"publisher": @"10" + }; + + NSArray *powersJSON = @[ + @{ + @"id": @"4", + @"name": @"Agility" + }, + @{ + @"id": @"9", + @"name": @"Insanely Rich" + } + ]; + + NSDictionary *publisherJSON = @{ + @"id": @"10", + @"name": @"DC Comics" + }; + + NSError *error = nil; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + [GRTJSONSerialization objectsWithEntityName:@"Power" fromJSONArray:powersJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + [GRTJSONSerialization objectWithEntityName:@"Publisher" fromJSONDictionary:publisherJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + XCTAssertEqual(2U, batman.powers.count, "should serialize to-many relationships"); + + GRTPower *agility = batman.powers[0]; + + XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); + + GRTPower *wealth = batman.powers[1]; + + XCTAssertEqualObjects(@9, wealth.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Insanely Rich", wealth.name, @"should serialize to-many relationships"); + + GRTPublisher *publisher = batman.publisher; + + XCTAssertNotNil(publisher, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@10, publisher.identifier, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@"DC Comics", publisher.name, @"should serialize to-one relationships"); +} + - (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; From 79c123998d7569e869b3d85c5b70665ab322b280 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 11:23:41 +0200 Subject: [PATCH 18/45] Add negative test for identifier serialization --- GrootTests/GRTJSONSerializationTests.m | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 41ef822..001aa6d 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -358,6 +358,22 @@ - (void)testRelationshipSerializationFromIdentifiers { XCTAssertEqualObjects(@"DC Comics", publisher.name, @"should serialize to-one relationships"); } +- (void)testSerializationFromIdentifiersFailsWithoutIdentityAttribute { + NSEntityDescription *characterEntity = self.store.managedObjectModel.entitiesByName[@"Character"]; + characterEntity.userInfo = @{}; // Remove the identity attribute name from the entity + + NSArray *charactersJSON = @[@"1699", @"1455"]; + + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; + + XCTAssertNil(characters); + XCTAssertNotNil(error); + + XCTAssertEqualObjects(GRTErrorDomain, error.domain); + XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); +} + - (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; From 25a053c931f46eaca97f01833b9e151b06553730 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 11:24:45 +0200 Subject: [PATCH 19/45] Update attribute description helper to work with JSON simple values --- Groot/Private/NSAttributeDescription+Groot.h | 4 ++-- Groot/Private/NSAttributeDescription+Groot.m | 22 ++++++++++++-------- Groot/Private/NSEntityDescription+Groot.m | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Groot/Private/NSAttributeDescription+Groot.h b/Groot/Private/NSAttributeDescription+Groot.h index 831871f..321fd2e 100644 --- a/Groot/Private/NSAttributeDescription+Groot.h +++ b/Groot/Private/NSAttributeDescription+Groot.h @@ -32,9 +32,9 @@ NS_ASSUME_NONNULL_BEGIN - (nullable NSValueTransformer *)grt_JSONTransformer; /** - Returns the value for this attribute in a given JSON object. + Returns the value for this attribute in a given JSON value. */ -- (nullable id)grt_valueInJSONDictionary:(NSDictionary *)dictionary; +- (nullable id)grt_valueForJSONValue:(id)value; /** Returns all the values for this attribute in a given JSON array. diff --git a/Groot/Private/NSAttributeDescription+Groot.m b/Groot/Private/NSAttributeDescription+Groot.m index 70b631d..e63bee8 100644 --- a/Groot/Private/NSAttributeDescription+Groot.m +++ b/Groot/Private/NSAttributeDescription+Groot.m @@ -30,8 +30,14 @@ - (nullable NSValueTransformer *)grt_JSONTransformer { return name != nil ? [NSValueTransformer valueTransformerForName:name] : nil; } -- (nullable id)grt_valueInJSONDictionary:(NSDictionary * __nonnull)dictionary { - id value = [self grt_rawValueInJSONDictionary:dictionary]; +- (nullable id)grt_valueForJSONValue:(id __nonnull)JSONValue { + id value = nil; + + if ([JSONValue isKindOfClass:[NSDictionary class]]) { + value = [self grt_rawValueInJSONDictionary:JSONValue]; + } else if ([JSONValue isKindOfClass:[NSNumber class]] || [JSONValue isKindOfClass:[NSString class]]) { + value = JSONValue; + } if (value != nil) { if (value == [NSNull null]) { @@ -53,13 +59,11 @@ - (nullable id)grt_valueInJSONDictionary:(NSDictionary * __nonnull)dictionary { - (NSArray * __nonnull)grt_valuesInJSONArray:(NSArray * __nonnull)array { NSMutableArray *values = [NSMutableArray arrayWithCapacity:array.count]; - for (NSDictionary *object in array) { - if ([object isKindOfClass:[NSDictionary class]]) { - id value = [self grt_valueInJSONDictionary:object]; - - if (value != nil) { - [values addObject:value]; - } + for (id object in array) { + id value = [self grt_valueForJSONValue:object]; + + if (value != nil) { + [values addObject:value]; } } diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index 973aee6..f7132c9 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -140,7 +140,7 @@ - (nullable NSAttributeDescription *)grt_identityAttribute { - (nullable id)grt_identifierInJSONDictionary:(NSDictionary *)dictionary { NSAttributeDescription *identityAttribute = [self grt_identityAttribute]; if (identityAttribute != nil) { - return [identityAttribute grt_valueInJSONDictionary:dictionary]; + return [identityAttribute grt_valueForJSONValue:dictionary]; } return nil; From b9759f5f874beda82f64d50285284ad8bb3a70ba Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 12:24:07 +0200 Subject: [PATCH 20/45] Test that identifier serialization validates identifiers --- GrootTests/GRTJSONSerializationTests.m | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 001aa6d..b19aaf7 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -374,6 +374,18 @@ - (void)testSerializationFromIdentifiersFailsWithoutIdentityAttribute { XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); } +- (void)testSerializationFromIdentifiersValidatesValues { + NSArray *charactersJSON = @[@"1699", [NSValue valueWithRange:NSMakeRange(0, 0)]]; + + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; + + XCTAssertNil(characters); + XCTAssertNotNil(error); + XCTAssertEqualObjects(NSCocoaErrorDomain, error.domain); + XCTAssertEqual(NSValidationMissingMandatoryPropertyError, error.code); +} + - (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; From 25560591d38e8de7d25ae9880ad66778069c9085 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 12:24:59 +0200 Subject: [PATCH 21/45] Implement serialization from a JSON array of identifiers --- Groot/Private/NSEntityDescription+Groot.m | 141 +++++++++++++++------- 1 file changed, 99 insertions(+), 42 deletions(-) diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index f7132c9..2d8f6bb 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -70,50 +70,42 @@ - (nullable NSArray *)grt_importJSONArray:(NSArray *)array } for (id obj in array) { - if (![obj isKindOfClass:[NSDictionary class]]) continue; - - NSDictionary *dictionary = obj; - NSManagedObject *managedObject = nil; - - if (mergeChanges) { - id identifier = [self grt_identifierInJSONDictionary:dictionary]; - if (identifier != nil) { - managedObject = existingObjects[identifier]; - } + if (obj == [NSNull null]) { + continue; } - if (managedObject == nil) { - managedObject = [[self class] insertNewObjectForEntityForName:self.name inManagedObjectContext:context]; - } + NSManagedObject *managedObject = [self grt_managedObjectForJSONValue:obj inContext:context existingObjects:existingObjects]; - [self.propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if (![property grt_JSONSerializable]) { - return; // continue - } - - if ([property isKindOfClass:[NSAttributeDescription class]]) { - NSAttributeDescription *attribute = (NSAttributeDescription *)property; - [managedObject grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; - } else if ([property isKindOfClass:[NSPropertyDescription class]]) { - NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; - [managedObject grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; - } - - *stop = (error != nil); // break on error - }]; + if ([obj isKindOfClass:[NSDictionary class]]) { + [self grt_importJSONDictionary:obj inManagedObject:managedObject mergeChanges:mergeChanges error:&error]; + } else { + [self grt_importJSONValue:obj inManagedObject:managedObject error:&error]; + } - if (error != nil) { + if (error == nil) { + [managedObjects addObject:managedObject]; + } else { [context deleteObject:managedObject]; return; // exit the block } - - [managedObjects addObject:managedObject]; } }]; if (error != nil) { - if (outError) *outError = error; - return nil; + // Delete any objects we have created when there's an error + if (managedObjects.count > 0) { + [context performBlockAndWait:^{ + for (NSManagedObject *object in managedObjects) { + [context deleteObject:object]; + } + }]; + } + + if (outError != nil) { + *outError = error; + } + + managedObjects = nil; } return managedObjects; @@ -137,15 +129,6 @@ - (nullable NSAttributeDescription *)grt_identityAttribute { return nil; } -- (nullable id)grt_identifierInJSONDictionary:(NSDictionary *)dictionary { - NSAttributeDescription *identityAttribute = [self grt_identityAttribute]; - if (identityAttribute != nil) { - return [identityAttribute grt_valueForJSONValue:dictionary]; - } - - return nil; -} - - (nullable NSDictionary *)grt_existingObjectsWithJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing __nullable * __nullable)outError @@ -187,6 +170,80 @@ - (nullable NSDictionary *)grt_existingObjectsWithJSONArray:(NSArray *)array return nil; } +- (NSManagedObject *)grt_managedObjectForJSONValue:(id)value + inContext:(NSManagedObjectContext *)context + existingObjects:(nullable NSDictionary *)existingObjects +{ + NSManagedObject *managedObject = nil; + + if (existingObjects) { + NSAttributeDescription *identityAttribute = [self grt_identityAttribute]; + id identifier = [identityAttribute grt_valueForJSONValue:value]; + if (identifier != nil) { + managedObject = existingObjects[identifier]; + } + } + + if (managedObject == nil) { + managedObject = [[self class] insertNewObjectForEntityForName:self.name inManagedObjectContext:context]; + } + + return managedObject; +} + +- (void)grt_importJSONDictionary:(NSDictionary *)dictionary + inManagedObject:(NSManagedObject *)managedObject + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError * __block error = nil; + + [self.propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { + if (![property grt_JSONSerializable]) { + return; // continue + } + + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + [managedObject grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } else if ([property isKindOfClass:[NSPropertyDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + [managedObject grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } + + *stop = (error != nil); // break on error + }]; + + if (error != nil && outError != nil) { + *outError = error; + } +} + +- (void)grt_importJSONValue:(id)value + inManagedObject:(NSManagedObject *)managedObject + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSAttributeDescription *attribute = [self grt_identityAttribute]; + + if (attribute == nil) { + if (outError) { + NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); + NSString *message = [NSString stringWithFormat:format, self.name]; + *outError = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorIdentityNotFound + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + return; + } + + id identifier = [attribute grt_valueForJSONValue:value]; + + if ([managedObject validateValue:&identifier forKey:attribute.name error:outError]) { + [managedObject setValue:identifier forKey:attribute.name]; + } +} + @end NS_ASSUME_NONNULL_END From 1bd2cafe8d19b99328b8c8c2456c64bc26a6fbab Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 12:54:20 +0200 Subject: [PATCH 22/45] Implement relationship serialization from identifiers --- Groot/Private/NSManagedObject+Groot.m | 26 ++++++++++++-------------- GrootTests/GRTJSONSerializationTests.m | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index cc80106..5cda7d0 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -70,21 +70,12 @@ - (void)grt_setRelationship:(NSRelationshipDescription *)relationship id value = nil; id rawValue = [relationship grt_rawValueInJSONDictionary:dictionary]; - if (rawValue != nil) { + if (rawValue != nil && rawValue != [NSNull null]) { NSError *error = nil; NSEntityDescription *destinationEntity = relationship.destinationEntity; + BOOL isArray = [rawValue isKindOfClass:[NSArray class]]; - if ([rawValue isKindOfClass:[NSDictionary class]] && !relationship.toMany) { - NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] - inContext:self.managedObjectContext - mergeChanges:mergeChanges - error:&error].firstObject; - - if (managedObject != nil) { - value = managedObject; - } - } - else if ([rawValue isKindOfClass:[NSArray class]] && relationship.toMany) { + if (relationship.toMany && isArray) { NSArray *managedObjects = [destinationEntity grt_importJSONArray:rawValue inContext:self.managedObjectContext mergeChanges:mergeChanges @@ -93,8 +84,15 @@ - (void)grt_setRelationship:(NSRelationshipDescription *)relationship value = relationship.ordered ? [NSOrderedSet orderedSetWithArray:managedObjects] : [NSSet setWithArray:managedObjects]; } } - else if (rawValue == [NSNull null]) { - // Nothing to do + else if (!relationship.toMany && !isArray) { + NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error].firstObject; + + if (managedObject != nil) { + value = managedObject; + } } else { NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index b19aaf7..043f2f3 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -99,7 +99,7 @@ - (void)testInsertInvalidToOneRelationship { @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"publisher": @"DC" // This should be a JSON dictionary + @"publisher": @[@"DC"] // This should be a JSON dictionary }; NSError *error = nil; From 516e5def52a8e9d3a0cb96767b39131400391231 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 20:23:10 +0200 Subject: [PATCH 23/45] Add tests for serialisation with entity inheritance --- Groot.xcodeproj/project.pbxproj | 4 ++ GrootTests/GRTJSONSerializationTests.m | 22 ++++++++++ GrootTests/GRTModels.h | 43 ++++++++++++++++--- GrootTests/GRTModels.m | 25 +++++++++++ GrootTests/JSON/container.json | 14 ++++++ .../Model.xcdatamodel/contents | 43 +++++++++++++++++-- 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 GrootTests/JSON/container.json diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index ebe3926..dfa655f 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B409C7301B5979E700C47D48 /* container.json in Resources */ = {isa = PBXBuildFile; fileRef = B409C72F1B5979E700C47D48 /* container.json */; }; B42A1D931B56600000309637 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; B42A1D941B56600000309637 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; B42A1D971B56614600309637 /* NSData+Resource.m in Sources */ = {isa = PBXBuildFile; fileRef = B42A1D961B56614600309637 /* NSData+Resource.m */; }; @@ -50,6 +51,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B409C72F1B5979E700C47D48 /* container.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = container.json; sourceTree = ""; }; B42A1D911B56600000309637 /* characters.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters.json; sourceTree = ""; }; B42A1D921B56600000309637 /* characters_update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters_update.json; sourceTree = ""; }; B42A1D951B56614600309637 /* NSData+Resource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Resource.h"; sourceTree = ""; }; @@ -111,6 +113,7 @@ children = ( B42A1D911B56600000309637 /* characters.json */, B42A1D921B56600000309637 /* characters_update.json */, + B409C72F1B5979E700C47D48 /* container.json */, ); path = JSON; sourceTree = ""; @@ -311,6 +314,7 @@ files = ( B42A1D941B56600000309637 /* characters_update.json in Resources */, B42A1D931B56600000309637 /* characters.json in Resources */, + B409C7301B5979E700C47D48 /* container.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 043f2f3..9fe3be0 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -386,6 +386,28 @@ - (void)testSerializationFromIdentifiersValidatesValues { XCTAssertEqual(NSValidationMissingMandatoryPropertyError, error.code); } +- (void)testSerializationWithEntityInheritance { + NSData *data = [NSData grt_dataWithContentsOfResource:@"container.json"]; + XCTAssertNotNil(data, @"container.json not found"); + + NSError *error = nil; + NSArray *objects = [GRTJSONSerialization objectsWithEntityName:@"Container" fromJSONData:data inContext:self.context error:&error]; + XCTAssertNil(error); + XCTAssertEqual(1U, objects.count); + + GRTContainer *container = objects[0]; + + GRTConcreteA *concreteA = container.abstracts[0]; + XCTAssertEqualObjects(@"ConcreteA", concreteA.entity.name); + XCTAssertEqualObjects(@1, concreteA.identifier); + XCTAssertEqualObjects(@"this is A", concreteA.foo); + + GRTConcreteB *concreteB = container.abstracts[1]; + XCTAssertEqualObjects(@"ConcreteB", concreteB.entity.name); + XCTAssertEqualObjects(@2, concreteB.identifier); + XCTAssertEqualObjects(@"this is B", concreteB.bar); +} + - (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; diff --git a/GrootTests/GRTModels.h b/GrootTests/GRTModels.h index 4af6c50..dfaf5c0 100644 --- a/GrootTests/GRTModels.h +++ b/GrootTests/GRTModels.h @@ -8,13 +8,15 @@ #import +NS_ASSUME_NONNULL_BEGIN + @class GRTPower, GRTPublisher; @interface GRTCharacter : NSManagedObject -@property (nonatomic, retain, nonnull) NSNumber *identifier; -@property (nonatomic, retain, nonnull) NSString *name; -@property (nonatomic, retain, nonnull) NSString *realName; +@property (nonatomic, retain) NSNumber *identifier; +@property (nonatomic, retain) NSString *name; +@property (nonatomic, retain) NSString *realName; @property (nonatomic, retain, nullable) NSOrderedSet *powers; @property (nonatomic, retain, nullable) GRTPublisher *publisher; @@ -22,22 +24,49 @@ @interface GRTPublisher : NSManagedObject -@property (nonatomic, retain, nonnull) NSNumber *identifier; -@property (nonatomic, retain, nonnull) NSString *name; +@property (nonatomic, retain) NSNumber *identifier; +@property (nonatomic, retain) NSString *name; @property (nonatomic, retain, nullable) NSSet *characters; @end @interface GRTPower : NSManagedObject -@property (nonatomic, retain, nonnull) NSNumber *identifier; -@property (nonatomic, retain, nonnull) NSString *name; +@property (nonatomic, retain) NSNumber *identifier; +@property (nonatomic, retain) NSString *name; @property (nonatomic, retain, nullable) NSSet *characters; @end +@interface GRTContainer : NSManagedObject + +@property (nonatomic, retain, nullable) NSOrderedSet *abstracts; + +@end + +@interface GRTAbstract : NSManagedObject + +@property (nonatomic, retain) NSNumber *identifier; +@property (nonatomic, retain, nullable) GRTContainer *container; + +@end + +@interface GRTConcreteA : GRTAbstract + +@property (nonatomic, retain) NSString *foo; + +@end + +@interface GRTConcreteB : GRTAbstract + +@property (nonatomic, retain) NSString *bar; + +@end + @interface NSManagedObjectModel (GrootTests) + (nonnull instancetype)grt_testModel; @end + +NS_ASSUME_NONNULL_END diff --git a/GrootTests/GRTModels.m b/GrootTests/GRTModels.m index a75503e..307d9fc 100644 --- a/GrootTests/GRTModels.m +++ b/GrootTests/GRTModels.m @@ -34,6 +34,31 @@ @implementation GRTPublisher @end +@implementation GRTContainer + +@dynamic abstracts; + +@end + +@implementation GRTAbstract + +@dynamic identifier; +@dynamic container; + +@end + +@implementation GRTConcreteA + +@dynamic foo; + +@end + +@implementation GRTConcreteB + +@dynamic bar; + +@end + @implementation NSManagedObjectModel (GrootTests) + (nonnull instancetype)grt_testModel { diff --git a/GrootTests/JSON/container.json b/GrootTests/JSON/container.json new file mode 100644 index 0000000..6bd3955 --- /dev/null +++ b/GrootTests/JSON/container.json @@ -0,0 +1,14 @@ +{ + "abstracts": [ + { + "id": 1, + "type": "A", + "foo": "this is A" + }, + { + "id": 2, + "type": "B", + "bar": "this is B" + } + ] +} diff --git a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents index df5cc66..ff2d2b9 100644 --- a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,17 @@ + + + + + + + + + + + + @@ -31,6 +43,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -66,8 +99,12 @@ - - - + + + + + + + \ No newline at end of file From 323993d921ae822a7d616876a57a7d80f4632217 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 22:45:13 +0200 Subject: [PATCH 24/45] Implement serialization with entity inheritance --- Groot/GRTJSONSerialization.m | 4 +- Groot/NSValueTransformer+Groot.h | 14 ++ Groot/NSValueTransformer+Groot.m | 6 + Groot/NSValueTransformer+Groot.swift | 25 ++- Groot/Private/NSEntityDescription+Groot.h | 2 +- Groot/Private/NSEntityDescription+Groot.m | 98 ++++------- Groot/Private/NSManagedObject+Groot.h | 14 +- Groot/Private/NSManagedObject+Groot.m | 192 ++++++++++++++-------- GrootTests/GRTJSONSerializationTests.m | 9 + GrootTests/NSValueTransformerTests.swift | 20 +++ 10 files changed, 231 insertions(+), 153 deletions(-) diff --git a/Groot/GRTJSONSerialization.m b/Groot/GRTJSONSerialization.m index 348a487..796bba7 100644 --- a/Groot/GRTJSONSerialization.m +++ b/Groot/GRTJSONSerialization.m @@ -67,7 +67,7 @@ + (nullable id)objectWithEntityName:(NSString *)entityName return nil; } - BOOL mergeChanges = [entity grt_hasIdentity]; + BOOL mergeChanges = [entity grt_identityAttribute] != nil; return [entity grt_importJSONArray:@[JSONDictionary] inContext:context @@ -88,7 +88,7 @@ + (nullable NSArray *)objectsWithEntityName:(NSString *)entityName return nil; } - BOOL mergeChanges = [entity grt_hasIdentity]; + BOOL mergeChanges = [entity grt_identityAttribute] != nil; return [entity grt_importJSONArray:JSONArray inContext:context diff --git a/Groot/NSValueTransformer+Groot.h b/Groot/NSValueTransformer+Groot.h index 3c587d3..a0ee2ee 100644 --- a/Groot/NSValueTransformer+Groot.h +++ b/Groot/NSValueTransformer+Groot.h @@ -48,6 +48,20 @@ typedef __nullable id (^GRTTransformBlock)(id value); transformBlock:(__nullable id (^)(id value))transformBlock reverseTransformBlock:(__nullable id (^)(id value))reverseTransformBlock; +/** + Registers an entity mapper with a given name and map block. + + An entity mapper maps a JSON dictionary to an entity name. + + Entity mappers can be associated with abstract core data entities in the user info + dictionary by using the `entityMapperName` key. + + @param name The name of the mapper. + @param mapBlock The block that performs the mapping. + */ ++ (void)grt_setEntityMapperWithName:(NSString *)name + mapBlock:(NSString * __nullable (^)(NSDictionary *JSONDictionary))mapBlock; + @end NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.m b/Groot/NSValueTransformer+Groot.m index c040b54..d04f715 100644 --- a/Groot/NSValueTransformer+Groot.m +++ b/Groot/NSValueTransformer+Groot.m @@ -42,6 +42,12 @@ + (void)grt_setValueTransformerWithName:(NSString *)name [self setValueTransformer:valueTransformer forName:name]; } ++ (void)grt_setEntityMapperWithName:(NSString *)name + mapBlock:(NSString * __nullable (^)(NSDictionary *JSONDictionary))mapBlock +{ + return [self grt_setValueTransformerWithName:name transformBlock:mapBlock]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.swift b/Groot/NSValueTransformer+Groot.swift index 57d05d8..726a1bd 100644 --- a/Groot/NSValueTransformer+Groot.swift +++ b/Groot/NSValueTransformer+Groot.swift @@ -38,11 +38,11 @@ public extension NSValueTransformer { } /** - Registers a reversible value transformer with a given name and transform functions. + Registers a reversible value transformer with a given name and transform functions. - :param: name The name of the transformer. - :param: transform The function that performs the forward transformation. - :param: reverseTransform The function that performs the reverse transformation. + :param: name The name of the transformer. + :param: transform The function that performs the forward transformation. + :param: reverseTransform The function that performs the reverse transformation. */ class func setValueTransformerWithName(name: String, transform: (T) -> (U?), reverseTransform: (U) -> (T?)) { grt_setValueTransformerWithName(name, transformBlock: { value in @@ -55,4 +55,21 @@ public extension NSValueTransformer { } }) } + + /** + Registers an entity mapper with a given name and map block. + + An entity mapper maps a JSON dictionary to an entity name. + + Entity mappers can be associated with abstract core data entities in the user info + dictionary by using the `entityMapperName` key. + + :param: name The name of the mapper. + :param: map The function that performs the mapping. + */ + class func setEntityMapperWithName(name: String, map: ([String: AnyObject]) -> (String?)) { + grt_setEntityMapperWithName(name) { dictionary in + return (dictionary as? [String: AnyObject]).flatMap(map) + } + } } diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 83dec2a..a6a2be5 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN inContext:(NSManagedObjectContext *)context error:(NSError * __nullable * __nullable)error; -- (BOOL)grt_hasIdentity; +- (nullable NSAttributeDescription *)grt_identityAttribute; - (nullable NSArray *)grt_importJSONArray:(NSArray *)array inContext:(NSManagedObjectContext *)context diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index 2d8f6bb..432e7f0 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -44,8 +44,20 @@ + (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName return entity; } -- (BOOL)grt_hasIdentity { - return [self grt_identityAttribute] != nil; +- (nullable NSAttributeDescription *)grt_identityAttribute { + NSString *attributeName = nil; + NSEntityDescription *entity = self; + + while (entity != nil && attributeName == nil) { + attributeName = entity.userInfo[@"identityAttribute"]; + entity = [entity superentity]; + } + + if (attributeName != nil) { + return self.attributesByName[attributeName]; + } + + return nil; } - (nullable NSArray *)grt_importJSONArray:(NSArray *)array @@ -77,9 +89,9 @@ - (nullable NSArray *)grt_importJSONArray:(NSArray *)array NSManagedObject *managedObject = [self grt_managedObjectForJSONValue:obj inContext:context existingObjects:existingObjects]; if ([obj isKindOfClass:[NSDictionary class]]) { - [self grt_importJSONDictionary:obj inManagedObject:managedObject mergeChanges:mergeChanges error:&error]; + [managedObject grt_importJSONDictionary:obj mergeChanges:mergeChanges error:&error]; } else { - [self grt_importJSONValue:obj inManagedObject:managedObject error:&error]; + [managedObject grt_importJSONValue:obj error:&error]; } if (error == nil) { @@ -113,20 +125,24 @@ - (nullable NSArray *)grt_importJSONArray:(NSArray *)array #pragma mark - Private -- (nullable NSAttributeDescription *)grt_identityAttribute { - NSString *attributeName = nil; - NSEntityDescription *entity = self; +- (nullable NSValueTransformer *)grt_entityMapper { + NSString *name = self.userInfo[@"entityMapperName"]; - while (entity != nil && attributeName == nil) { - attributeName = entity.userInfo[@"identityAttribute"]; - entity = [entity superentity]; + if (name == nil) { + return nil; } - if (attributeName != nil) { - return self.attributesByName[attributeName]; + return [NSValueTransformer valueTransformerForName:name]; +} + +- (NSString *)grt_entityNameForJSONValue:(id)value { + NSString *name = nil; + + if ([value isKindOfClass:[NSDictionary class]]) { + name = [[self grt_entityMapper] transformedValue:value]; } - return nil; + return name ? : self.name; } - (nullable NSDictionary *)grt_existingObjectsWithJSONArray:(NSArray *)array @@ -185,65 +201,13 @@ - (NSManagedObject *)grt_managedObjectForJSONValue:(id)value } if (managedObject == nil) { - managedObject = [[self class] insertNewObjectForEntityForName:self.name inManagedObjectContext:context]; + NSString *entityName = [self grt_entityNameForJSONValue:value]; + managedObject = [[self class] insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; } return managedObject; } -- (void)grt_importJSONDictionary:(NSDictionary *)dictionary - inManagedObject:(NSManagedObject *)managedObject - mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)outError -{ - NSError * __block error = nil; - - [self.propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if (![property grt_JSONSerializable]) { - return; // continue - } - - if ([property isKindOfClass:[NSAttributeDescription class]]) { - NSAttributeDescription *attribute = (NSAttributeDescription *)property; - [managedObject grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; - } else if ([property isKindOfClass:[NSPropertyDescription class]]) { - NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; - [managedObject grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; - } - - *stop = (error != nil); // break on error - }]; - - if (error != nil && outError != nil) { - *outError = error; - } -} - -- (void)grt_importJSONValue:(id)value - inManagedObject:(NSManagedObject *)managedObject - error:(NSError *__autoreleasing __nullable * __nullable)outError -{ - NSAttributeDescription *attribute = [self grt_identityAttribute]; - - if (attribute == nil) { - if (outError) { - NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); - NSString *message = [NSString stringWithFormat:format, self.name]; - *outError = [NSError errorWithDomain:GRTErrorDomain - code:GRTErrorIdentityNotFound - userInfo:@{ NSLocalizedDescriptionKey: message }]; - } - - return; - } - - id identifier = [attribute grt_valueForJSONValue:value]; - - if ([managedObject validateValue:&identifier forKey:attribute.name error:outError]) { - [managedObject setValue:identifier forKey:attribute.name]; - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSManagedObject+Groot.h b/Groot/Private/NSManagedObject+Groot.h index 70ec17d..5bff6bd 100644 --- a/Groot/Private/NSManagedObject+Groot.h +++ b/Groot/Private/NSManagedObject+Groot.h @@ -26,15 +26,11 @@ NS_ASSUME_NONNULL_BEGIN @interface NSManagedObject (Groot) -- (void)grt_setAttribute:(NSAttributeDescription *)attribute - fromJSONDictionary:(NSDictionary *)dictionary - mergeChanges:(BOOL)mergeChanges - error:(NSError * __nullable * __nullable)error; - -- (void)grt_setRelationship:(NSRelationshipDescription *)relationship - fromJSONDictionary:(NSDictionary *)dictionary - mergeChanges:(BOOL)mergeChanges - error:(NSError * __nullable * __nullable)error; +- (void)grt_importJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)error; + +- (void)grt_importJSONValue:(id)value error:(NSError *__autoreleasing __nullable * __nullable)error; - (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships; diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m index 5cda7d0..77a4a81 100644 --- a/Groot/Private/NSManagedObject+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.m @@ -32,90 +32,53 @@ @implementation NSManagedObject (Groot) -- (void)grt_setAttribute:(NSAttributeDescription *)attribute - fromJSONDictionary:(NSDictionary *)dictionary - mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)outError +- (void)grt_importJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError { - id value = nil; - id rawValue = [attribute grt_rawValueInJSONDictionary:dictionary]; + NSError * __block error = nil; + NSDictionary *propertiesByName = self.entity.propertiesByName; - if (rawValue != nil) { - if (rawValue != [NSNull null]) { - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - if (transformer) { - value = [transformer transformedValue:rawValue]; - } else { - value = rawValue; - } + [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { + if (![property grt_JSONSerializable]) { + return; // continue } - } else if (mergeChanges) { - // Just validate the current value - value = [self valueForKey:attribute.name]; - [self validateValue:&value forKey:attribute.name error:outError]; - return; - } + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + [self grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } else if ([property isKindOfClass:[NSRelationshipDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + [self grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } + + *stop = (error != nil); // break on error + }]; - if ([self validateValue:&value forKey:attribute.name error:outError]) { - [self setValue:value forKey:attribute.name]; + if (error != nil && outError != nil) { + *outError = error; } } -- (void)grt_setRelationship:(NSRelationshipDescription *)relationship - fromJSONDictionary:(NSDictionary *)dictionary - mergeChanges:(BOOL)mergeChanges - error:(NSError *__autoreleasing __nullable * __nullable)outError -{ - id value = nil; - id rawValue = [relationship grt_rawValueInJSONDictionary:dictionary]; +- (void)grt_importJSONValue:(id)value error:(NSError *__autoreleasing __nullable * __nullable)outError { + NSAttributeDescription *attribute = [self.entity grt_identityAttribute]; - if (rawValue != nil && rawValue != [NSNull null]) { - NSError *error = nil; - NSEntityDescription *destinationEntity = relationship.destinationEntity; - BOOL isArray = [rawValue isKindOfClass:[NSArray class]]; - - if (relationship.toMany && isArray) { - NSArray *managedObjects = [destinationEntity grt_importJSONArray:rawValue - inContext:self.managedObjectContext - mergeChanges:mergeChanges - error:&error]; - if (managedObjects != nil) { - value = relationship.ordered ? [NSOrderedSet orderedSetWithArray:managedObjects] : [NSSet setWithArray:managedObjects]; - } + if (attribute == nil) { + if (outError) { + NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); + NSString *message = [NSString stringWithFormat:format, self.entity.name]; + *outError = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorIdentityNotFound + userInfo:@{ NSLocalizedDescriptionKey: message }]; } - else if (!relationship.toMany && !isArray) { - NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] - inContext:self.managedObjectContext - mergeChanges:mergeChanges - error:&error].firstObject; - - if (managedObject != nil) { - value = managedObject; - } - } - else { - NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); - NSString *message = [NSString stringWithFormat:format, rawValue, relationship.entity.name, relationship.name]; - error = [NSError errorWithDomain:GRTErrorDomain - code:GRTErrorInvalidJSONObject - userInfo:@{ NSLocalizedDescriptionKey: message }]; - } - - if (error != nil) { - if (outError != nil) *outError = error; - return; - } - } else if (mergeChanges) { - // Just validate the current value - value = [self valueForKey:relationship.name]; - [self validateValue:&value forKey:relationship.name error:outError]; return; } - if ([self validateValue:&value forKey:relationship.name error:outError]) { - [self setValue:value forKey:relationship.name]; + id identifier = [attribute grt_valueForJSONValue:value]; + + if ([self validateValue:&identifier forKey:attribute.name error:outError]) { + [self setValue:identifier forKey:attribute.name]; } } @@ -202,6 +165,95 @@ - (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)ser return [dictionary copy]; } +#pragma mark - Private + +- (void)grt_setAttribute:(NSAttributeDescription *)attribute + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + id value = nil; + id rawValue = [attribute grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil) { + if (rawValue != [NSNull null]) { + NSValueTransformer *transformer = [attribute grt_JSONTransformer]; + if (transformer) { + value = [transformer transformedValue:rawValue]; + } else { + value = rawValue; + } + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:attribute.name]; + [self validateValue:&value forKey:attribute.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:attribute.name error:outError]) { + [self setValue:value forKey:attribute.name]; + } +} + +- (void)grt_setRelationship:(NSRelationshipDescription *)relationship + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + id value = nil; + id rawValue = [relationship grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil && rawValue != [NSNull null]) { + NSError *error = nil; + NSEntityDescription *destinationEntity = relationship.destinationEntity; + BOOL isArray = [rawValue isKindOfClass:[NSArray class]]; + + if (relationship.toMany && isArray) { + NSArray *managedObjects = [destinationEntity grt_importJSONArray:rawValue + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error]; + if (managedObjects != nil) { + value = relationship.ordered ? [NSOrderedSet orderedSetWithArray:managedObjects] : [NSSet setWithArray:managedObjects]; + } + } + else if (!relationship.toMany && !isArray) { + NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error].firstObject; + + if (managedObject != nil) { + value = managedObject; + } + } + else { + NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); + NSString *message = [NSString stringWithFormat:format, rawValue, relationship.entity.name, relationship.name]; + error = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorInvalidJSONObject + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + if (error != nil) { + if (outError != nil) *outError = error; + return; + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:relationship.name]; + [self validateValue:&value forKey:relationship.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:relationship.name error:outError]) { + [self setValue:value forKey:relationship.name]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 9fe3be0..cbe8627 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -34,6 +34,15 @@ - (void)setUp { } reverseTransformBlock:^id(NSNumber *value) { return [value stringValue]; }]; + + [NSValueTransformer grt_setEntityMapperWithName:@"GrootTests.Abstract" mapBlock:^NSString *(NSDictionary *JSONDictionary) { + NSDictionary *entityMapping = @{ + @"A": @"ConcreteA", + @"B": @"ConcreteB" + }; + NSString *type = JSONDictionary[@"type"]; + return entityMapping[type]; + }]; } - (void)tearDown { diff --git a/GrootTests/NSValueTransformerTests.swift b/GrootTests/NSValueTransformerTests.swift index 5053248..a6cd36f 100644 --- a/GrootTests/NSValueTransformerTests.swift +++ b/GrootTests/NSValueTransformerTests.swift @@ -46,4 +46,24 @@ class NSValueTransformerTests: XCTestCase { XCTAssertNil(transformer.reverseTransformedValue("not a number"), "should handle unsupported values") } + func testEntityMapper() { + func entityForJSONDictionary(dictionary: [String: AnyObject]) -> String? { + let entitiesByType = [ + "A": "ConcreteA", + "B": "ConcreteB" + ] + + if let type = dictionary["type"] as? String { + return entitiesByType[type] + } + return nil + } + + NSValueTransformer.setEntityMapperWithName("testEntityMapper", map: entityForJSONDictionary) + + let transformer = NSValueTransformer(forName: "testEntityMapper")! + XCTAssertEqual("ConcreteA", transformer.transformedValue(["type": "A"]) as! String, "should call the transform function") + XCTAssertEqual("ConcreteB", transformer.transformedValue(["type": "B"]) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + } } From 218663b062c2eb4b27bcab4b68c3627e2894e4f6 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 22:51:12 +0200 Subject: [PATCH 25/45] Minor change for entity mapping --- Groot/NSValueTransformer+Groot.swift | 7 +++++-- GrootTests/NSValueTransformerTests.swift | 14 ++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Groot/NSValueTransformer+Groot.swift b/Groot/NSValueTransformer+Groot.swift index 726a1bd..3d027da 100644 --- a/Groot/NSValueTransformer+Groot.swift +++ b/Groot/NSValueTransformer+Groot.swift @@ -68,8 +68,11 @@ public extension NSValueTransformer { :param: map The function that performs the mapping. */ class func setEntityMapperWithName(name: String, map: ([String: AnyObject]) -> (String?)) { - grt_setEntityMapperWithName(name) { dictionary in - return (dictionary as? [String: AnyObject]).flatMap(map) + grt_setEntityMapperWithName(name) { value in + if let dictionary = value as? [String: AnyObject] { + return map(dictionary) + } + return nil } } } diff --git a/GrootTests/NSValueTransformerTests.swift b/GrootTests/NSValueTransformerTests.swift index a6cd36f..9c0ed32 100644 --- a/GrootTests/NSValueTransformerTests.swift +++ b/GrootTests/NSValueTransformerTests.swift @@ -48,13 +48,15 @@ class NSValueTransformerTests: XCTestCase { func testEntityMapper() { func entityForJSONDictionary(dictionary: [String: AnyObject]) -> String? { - let entitiesByType = [ - "A": "ConcreteA", - "B": "ConcreteB" - ] - if let type = dictionary["type"] as? String { - return entitiesByType[type] + switch type { + case "A": + return "ConcreteA" + case "B": + return "ConcreteB" + default: + return nil + } } return nil } From 54026385bdec98ba2584dfddbd2d99b999112311 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Fri, 17 Jul 2015 23:00:21 +0200 Subject: [PATCH 26/45] Test updating subentity --- GrootTests/GRTJSONSerializationTests.m | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index cbe8627..fd46462 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -415,6 +415,17 @@ - (void)testSerializationWithEntityInheritance { XCTAssertEqualObjects(@"ConcreteB", concreteB.entity.name); XCTAssertEqualObjects(@2, concreteB.identifier); XCTAssertEqualObjects(@"this is B", concreteB.bar); + + NSDictionary *updateConcreteA = @{ + @"id": @1, + @"foo": @"A has been updated" + }; + + concreteA = [GRTJSONSerialization objectWithEntityName:@"Abstract" fromJSONDictionary:updateConcreteA inContext:self.context error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(@"ConcreteA", concreteA.entity.name); + XCTAssertEqualObjects(@1, concreteA.identifier); + XCTAssertEqualObjects(@"A has been updated", concreteA.foo); } - (void)testSerializationToJSON { From 5215ea7529735e7c5788a4bbd3e016dee3f4b29f Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sat, 18 Jul 2015 18:43:17 +0200 Subject: [PATCH 27/45] Add OS X target --- Groot.xcodeproj/project.pbxproj | 275 ++++++++++++++++++ .../xcshareddata/xcschemes/Groot-Mac.xcscheme | 110 +++++++ 2 files changed, 385 insertions(+) create mode 100644 Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index dfa655f..2ec823a 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,6 +7,37 @@ objects = { /* Begin PBXBuildFile section */ + B408736D1B5AB6930063F150 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B40873621B5AB6930063F150 /* Groot.framework */; }; + B408737B1B5AB8350063F150 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B408737C1B5AB83D0063F150 /* Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FD30701B569F1A002392F7 /* Groot.swift */; }; + B408737D1B5AB8470063F150 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B408737E1B5AB8470063F150 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; + B408737F1B5AB8470063F150 /* GRTJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873801B5AB8470063F150 /* GRTJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */; }; + B40873811B5AB8470063F150 /* GRTManagedStore.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873821B5AB8470063F150 /* GRTManagedStore.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */; }; + B40873831B5AB8470063F150 /* NSValueTransformer+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873841B5AB8470063F150 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; + B40873851B5AB8470063F150 /* NSValueTransformer+Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */; }; + B40873861B5AB87F0063F150 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */; }; + B40873871B5AB87F0063F150 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */; }; + B40873881B5AB87F0063F150 /* NSPropertyDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */; }; + B40873891B5AB87F0063F150 /* NSPropertyDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */; }; + B408738A1B5AB87F0063F150 /* NSAttributeDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */; }; + B408738B1B5AB87F0063F150 /* NSAttributeDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */; }; + B408738C1B5AB87F0063F150 /* NSEntityDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */; }; + B408738D1B5AB87F0063F150 /* NSEntityDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */; }; + B408738E1B5AB87F0063F150 /* NSManagedObject+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */; }; + B408738F1B5AB87F0063F150 /* NSManagedObject+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */; }; + B40873901B5AB89C0063F150 /* NSValueTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */; }; + B40873911B5AB8A20063F150 /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; + B40873921B5AB8A60063F150 /* GRTJSONSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */; }; + B40873931B5AB8AF0063F150 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; + B40873941B5AB8AF0063F150 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; + B40873951B5AB8AF0063F150 /* container.json in Resources */ = {isa = PBXBuildFile; fileRef = B409C72F1B5979E700C47D48 /* container.json */; }; + B408739A1B5AB8DC0063F150 /* GRTModels.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */; }; + B408739B1B5AB8E20063F150 /* NSData+Resource.m in Sources */ = {isa = PBXBuildFile; fileRef = B42A1D961B56614600309637 /* NSData+Resource.m */; }; + B408739C1B5AB8E80063F150 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */; }; B409C7301B5979E700C47D48 /* container.json in Resources */ = {isa = PBXBuildFile; fileRef = B409C72F1B5979E700C47D48 /* container.json */; }; B42A1D931B56600000309637 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; B42A1D941B56600000309637 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; @@ -41,6 +72,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B408736E1B5AB6930063F150 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B4DC1AE31AA9CA5E00F67403 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B40873611B5AB6930063F150; + remoteInfo = "Groot-Mac"; + }; B4DC1AF91AA9CA5E00F67403 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B4DC1AE31AA9CA5E00F67403 /* Project object */; @@ -51,6 +89,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B40873621B5AB6930063F150 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B408736C1B5AB6930063F150 /* GrootTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GrootTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B409C72F1B5979E700C47D48 /* container.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = container.json; sourceTree = ""; }; B42A1D911B56600000309637 /* characters.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters.json; sourceTree = ""; }; B42A1D921B56600000309637 /* characters_update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters_update.json; sourceTree = ""; }; @@ -90,6 +130,21 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B408735E1B5AB6930063F150 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B40873691B5AB6930063F150 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B408736D1B5AB6930063F150 /* Groot.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE81AA9CA5E00F67403 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -132,6 +187,8 @@ children = ( B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */, B4DC1AF71AA9CA5E00F67403 /* GrootTests.xctest */, + B40873621B5AB6930063F150 /* Groot.framework */, + B408736C1B5AB6930063F150 /* GrootTests.xctest */, ); name = Products; sourceTree = ""; @@ -209,6 +266,23 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + B408735F1B5AB6930063F150 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B408738C1B5AB87F0063F150 /* NSEntityDescription+Groot.h in Headers */, + B408737B1B5AB8350063F150 /* Groot.h in Headers */, + B40873831B5AB8470063F150 /* NSValueTransformer+Groot.h in Headers */, + B408737D1B5AB8470063F150 /* GRTError.h in Headers */, + B408738A1B5AB87F0063F150 /* NSAttributeDescription+Groot.h in Headers */, + B40873881B5AB87F0063F150 /* NSPropertyDescription+Groot.h in Headers */, + B408738E1B5AB87F0063F150 /* NSManagedObject+Groot.h in Headers */, + B40873811B5AB8470063F150 /* GRTManagedStore.h in Headers */, + B40873861B5AB87F0063F150 /* GRTValueTransformer.h in Headers */, + B408737F1B5AB8470063F150 /* GRTJSONSerialization.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE91AA9CA5E00F67403 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -229,6 +303,42 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + B40873611B5AB6930063F150 /* Groot-Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = B40873791B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-Mac" */; + buildPhases = ( + B408735D1B5AB6930063F150 /* Sources */, + B408735E1B5AB6930063F150 /* Frameworks */, + B408735F1B5AB6930063F150 /* Headers */, + B40873601B5AB6930063F150 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Groot-Mac"; + productName = "Groot-Mac"; + productReference = B40873621B5AB6930063F150 /* Groot.framework */; + productType = "com.apple.product-type.framework"; + }; + B408736B1B5AB6930063F150 /* Groot-MacTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B408737A1B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-MacTests" */; + buildPhases = ( + B40873681B5AB6930063F150 /* Sources */, + B40873691B5AB6930063F150 /* Frameworks */, + B408736A1B5AB6930063F150 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B408736F1B5AB6930063F150 /* PBXTargetDependency */, + ); + name = "Groot-MacTests"; + productName = "Groot-MacTests"; + productReference = B408736C1B5AB6930063F150 /* GrootTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; B4DC1AEB1AA9CA5E00F67403 /* Groot */ = { isa = PBXNativeTarget; buildConfigurationList = B4DC1B021AA9CA5E00F67403 /* Build configuration list for PBXNativeTarget "Groot" */; @@ -274,6 +384,12 @@ LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Guillermo Gonzalez"; TargetAttributes = { + B40873611B5AB6930063F150 = { + CreatedOnToolsVersion = 6.4; + }; + B408736B1B5AB6930063F150 = { + CreatedOnToolsVersion = 6.4; + }; B4DC1AEB1AA9CA5E00F67403 = { CreatedOnToolsVersion = 6.1.1; }; @@ -296,11 +412,30 @@ targets = ( B4DC1AEB1AA9CA5E00F67403 /* Groot */, B4DC1AF61AA9CA5E00F67403 /* GrootTests */, + B40873611B5AB6930063F150 /* Groot-Mac */, + B408736B1B5AB6930063F150 /* Groot-MacTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + B40873601B5AB6930063F150 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B408736A1B5AB6930063F150 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B40873941B5AB8AF0063F150 /* characters_update.json in Resources */, + B40873951B5AB8AF0063F150 /* container.json in Resources */, + B40873931B5AB8AF0063F150 /* characters.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AEA1AA9CA5E00F67403 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -321,6 +456,37 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B408735D1B5AB6930063F150 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B408738D1B5AB87F0063F150 /* NSEntityDescription+Groot.m in Sources */, + B40873891B5AB87F0063F150 /* NSPropertyDescription+Groot.m in Sources */, + B40873851B5AB8470063F150 /* NSValueTransformer+Groot.swift in Sources */, + B40873801B5AB8470063F150 /* GRTJSONSerialization.m in Sources */, + B40873841B5AB8470063F150 /* NSValueTransformer+Groot.m in Sources */, + B408737E1B5AB8470063F150 /* GRTError.m in Sources */, + B408737C1B5AB83D0063F150 /* Groot.swift in Sources */, + B408738B1B5AB87F0063F150 /* NSAttributeDescription+Groot.m in Sources */, + B40873871B5AB87F0063F150 /* GRTValueTransformer.m in Sources */, + B408738F1B5AB87F0063F150 /* NSManagedObject+Groot.m in Sources */, + B40873821B5AB8470063F150 /* GRTManagedStore.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B40873681B5AB6930063F150 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B40873901B5AB89C0063F150 /* NSValueTransformerTests.swift in Sources */, + B408739B1B5AB8E20063F150 /* NSData+Resource.m in Sources */, + B40873921B5AB8A60063F150 /* GRTJSONSerializationTests.m in Sources */, + B40873911B5AB8A20063F150 /* GRTManagedStoreTests.m in Sources */, + B408739A1B5AB8DC0063F150 /* GRTModels.m in Sources */, + B408739C1B5AB8E80063F150 /* Model.xcdatamodeld in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE71AA9CA5E00F67403 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -355,6 +521,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + B408736F1B5AB6930063F150 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B40873611B5AB6930063F150 /* Groot-Mac */; + targetProxy = B408736E1B5AB6930063F150 /* PBXContainerItemProxy */; + }; B4DC1AFA1AA9CA5E00F67403 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B4DC1AEB1AA9CA5E00F67403 /* Groot */; @@ -363,6 +534,94 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + B40873751B5AB6930063F150 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Groot/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_NAME = Groot; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + B40873761B5AB6930063F150 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = Groot/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_NAME = Groot; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + B40873771B5AB6930063F150 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = GrootTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_NAME = GrootTests; + SDKROOT = macosx; + }; + name = Debug; + }; + B40873781B5AB6930063F150 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = GrootTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_NAME = GrootTests; + SDKROOT = macosx; + }; + name = Release; + }; B4DC1B001AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -518,6 +777,22 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B40873791B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-Mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B40873751B5AB6930063F150 /* Debug */, + B40873761B5AB6930063F150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + B408737A1B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-MacTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B40873771B5AB6930063F150 /* Debug */, + B40873781B5AB6930063F150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; B4DC1AE61AA9CA5E00F67403 /* Build configuration list for PBXProject "Groot" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme new file mode 100644 index 0000000..910425a --- /dev/null +++ b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b565a2feab35447ede2883632bce75194b951047 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sat, 18 Jul 2015 21:42:54 +0200 Subject: [PATCH 28/45] Update pod spec and license --- Groot.podspec | 33 ++++++++++++++++++++++----------- LICENSE | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Groot.podspec b/Groot.podspec index 9e09b64..63842d6 100644 --- a/Groot.podspec +++ b/Groot.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Groot" - s.version = "0.2" + s.version = "1.0" s.summary = "From JSON to Core Data and back." s.description = <<-DESC @@ -12,16 +12,27 @@ Pod::Spec.new do |s| s.author = { "Guillermo Gonzalez" => "gonzalezreal@icloud.com" } s.social_media_url = "https://twitter.com/gonzalezreal" - - s.ios.deployment_target = '6.0' - s.osx.deployment_target = '10.8' - + s.source = { :git => "https://github.com/gonzalezreal/Groot.git", :tag => s.version.to_s } - - s.source_files = "Groot/**/*.{h,m}" - s.private_header_files = "Groot/Private/*.h" - - s.frameworks = 'Foundation', 'CoreData' - + + s.default_subspec = "Swift" + + s.subspec "Swift" do |ss| + ss.ios.deployment_target = "7.0" + ss.osx.deployment_target = "10.9" + + ss.source_files = "Groot/**/*.{swift,h,m}" + ss.private_header_files = "Groot/Private/*.h" + end + + s.subspec "ObjC" do |ss| + ss.ios.deployment_target = "6.0" + ss.osx.deployment_target = "10.8" + + ss.source_files = "Groot/**/*.{h,m}" + ss.private_header_files = "Groot/Private/*.h" + end + + s.frameworks = "Foundation", "CoreData" s.requires_arc = true end diff --git a/LICENSE b/LICENSE index 5ed077c..7c85695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Groot -Copyright (c) 2014 Guillermo Gonzalez +Copyright (c) 2014-2015 Guillermo Gonzalez Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 4fe0e17827fedf6d26778f0e03c4579c3f3de47f Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 11:01:05 +0200 Subject: [PATCH 29/45] Update mac scheme --- .../xcshareddata/xcschemes/Groot-Mac.xcscheme | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme index 910425a..c0643d6 100644 --- a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme +++ b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme @@ -15,7 +15,7 @@ @@ -29,7 +29,7 @@ @@ -47,7 +47,7 @@ @@ -57,7 +57,7 @@ @@ -76,7 +76,7 @@ @@ -94,7 +94,7 @@ From fbac24f43034f9be2109a1be786bd569adb4d3de Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 11:01:05 +0200 Subject: [PATCH 30/45] Update mac scheme --- .../xcshareddata/xcschemes/Groot-Mac.xcscheme | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme index 910425a..c0643d6 100644 --- a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme +++ b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme @@ -15,7 +15,7 @@ @@ -29,7 +29,7 @@ @@ -47,7 +47,7 @@ @@ -57,7 +57,7 @@ @@ -76,7 +76,7 @@ @@ -94,7 +94,7 @@ From 8d7c89d0a70b7a07a0c69cc6709a395ebf5c5a9d Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 11:06:16 +0200 Subject: [PATCH 31/45] Update license --- LICENSE | 21 --------------------- LICENSE.md | 9 +++++++++ 2 files changed, 9 insertions(+), 21 deletions(-) delete mode 100644 LICENSE create mode 100644 LICENSE.md diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7c85695..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Groot - -Copyright (c) 2014-2015 Guillermo Gonzalez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..55538c3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +**Groot** + +**Copyright (c) Guillermo Gonzalez** + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 2e3b91b29276cfe6ea6d0fb5a0661007b5112699 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 13:50:05 +0200 Subject: [PATCH 32/45] Add documentation for JSONKeyPath annotation --- Documentation/Annotations.md | 55 ++++++++++++++++++++++++++++++++++++ Documentation/README.md | 1 + 2 files changed, 56 insertions(+) create mode 100644 Documentation/Annotations.md create mode 100644 Documentation/README.md diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md new file mode 100644 index 0000000..a734bad --- /dev/null +++ b/Documentation/Annotations.md @@ -0,0 +1,55 @@ +# Annotations + +Entities, attributes and relationships in a managed object model have an associated **user info dictionary** in which you can specify custom metadata as key-value pairs. + +Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as **annotations**. + +You can use the **Data Model inspector** in Xcode to annotate entities, attributes and relationships: + +Data Model inspector + +This document lists all the different keys you can use to annotate models, and the purpose of each. + +## Property annotations + +### `JSONKeyPath` + +Using this key you can specify how your managed object’s properties (that is, attributes and relationships) map to key paths in a JSON object. + +For example, consider this JSON modelling a famous comic book character: + +```json +{ + "id": "1699", + "name": "Batman", + "publisher": { + "id": "10", + "name": "DC Comics" + } +} +``` + +We could model this in Core Data using two related entities: `Character` and `Publisher`. + +The `Character` entity could have `identifier` and `name` attributes, and a `publisher` to-one relationship. + +The `Publisher` entity could have `identifier` and `name` attributes, and a `characters` to-many relationship. + +Each of these properties should have a `JSONKeyPath` entry in their corresponding user info dictionary: + +* `id` for the `identifier` attribute, +* `name` for the `name` attribute, +* `publisher` for the `publisher` relationship, +* etc. + +Attributes and relationships that don't have a `JSONKeyPath` entry are **not considered** for JSON serialization or deserialization. + +Note that if we were only interested in the publisher's name, we could drop the `Publisher` entity and add a `publisherName` attribute specifying `publisher.name` as the `JSONKeyPath`. + +### `JSONTransformerName` + +## Entity annotations + +### `identityAttribute` + +### `entityMapperName` diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 0000000..16828fe --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1 @@ +This folder contains extended documentation for using Groot. \ No newline at end of file From 6ded12eff6608a91f0e71ee660c75ba61c7b9142 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 16:09:50 +0200 Subject: [PATCH 33/45] Add documentation for JSONTransformerName --- Documentation/Annotations.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md index a734bad..2aa65cf 100644 --- a/Documentation/Annotations.md +++ b/Documentation/Annotations.md @@ -48,6 +48,32 @@ Note that if we were only interested in the publisher's name, we could drop the ### `JSONTransformerName` +With this key you can specify the name of a value transformer that will be used to transform values when serializing from or into JSON. + +Consider the `id` key in the previous JSON. Some web APIs send 64-bit integers as strings to support languages that have trouble consuming large integers. + +We can declare the `identifier` attribute in both the `Character` and `Publisher` entities as a 64-bit integer to save some storage space. + +Then we can add a `JSONTransformerName` entry to the attribute's user info dictionary with the name of the value transformer: `StringToInteger`. + +Finally we can create the value transformer and give it the name we just used: + +```objc +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +} reverseTransformBlock:^id(NSNumber *value) { + return [value stringValue]; +}]; +``` + +If you don't need to serialize managed objects back into JSON, you don't need to specify a reverse transformation: + +```objc +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +}]; +``` + ## Entity annotations ### `identityAttribute` From 8561623417e4da732c65ceb744250c42be05f582 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 16:15:17 +0200 Subject: [PATCH 34/45] Fix typo --- Documentation/Annotations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md index 2aa65cf..7abb07a 100644 --- a/Documentation/Annotations.md +++ b/Documentation/Annotations.md @@ -54,7 +54,7 @@ Consider the `id` key in the previous JSON. Some web APIs send 64-bit integers a We can declare the `identifier` attribute in both the `Character` and `Publisher` entities as a 64-bit integer to save some storage space. -Then we can add a `JSONTransformerName` entry to the attribute's user info dictionary with the name of the value transformer: `StringToInteger`. +Then we can add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary with the name of the value transformer: `StringToInteger`. Finally we can create the value transformer and give it the name we just used: From 470f736d5d02162006544d3332dcb452c4a1d45b Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 16:22:51 +0200 Subject: [PATCH 35/45] Update JSONTransformerName documentation --- Documentation/Annotations.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md index 7abb07a..be3170f 100644 --- a/Documentation/Annotations.md +++ b/Documentation/Annotations.md @@ -52,11 +52,13 @@ With this key you can specify the name of a value transformer that will be used Consider the `id` key in the previous JSON. Some web APIs send 64-bit integers as strings to support languages that have trouble consuming large integers. -We can declare the `identifier` attribute in both the `Character` and `Publisher` entities as a 64-bit integer to save some storage space. +We should store identifier values as integers instead of strings to save space. -Then we can add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary with the name of the value transformer: `StringToInteger`. +First we need to change the `identifier` attribute's type to `Integer 64` in both the `Character` and `Publisher` entities. -Finally we can create the value transformer and give it the name we just used: +Then we add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary with the name of the value transformer: `StringToInteger`. + +Finally we create the value transformer and give it the name we just used: ```objc [NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { @@ -66,7 +68,7 @@ Finally we can create the value transformer and give it the name we just used: }]; ``` -If you don't need to serialize managed objects back into JSON, you don't need to specify a reverse transformation: +If we were not interested in serializing characters back into JSON we could omit the reverse transformation: ```objc [NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { From a2090eb3e1f6cedbffbf56f96030b04e419ac290 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 16:42:05 +0200 Subject: [PATCH 36/45] Add identityAttribute documentation --- Documentation/Annotations.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md index be3170f..be996e1 100644 --- a/Documentation/Annotations.md +++ b/Documentation/Annotations.md @@ -80,4 +80,12 @@ If we were not interested in serializing characters back into JSON we could omit ### `identityAttribute` +Use this key to specify the name of the attribute that uniquely identifies instances of an entity. + +In our example, we should add an `identityAttribute` entry to both the `Character` and `Publisher` entities user dictionaries with the value `identifier`. + +Specifying the `identityAttribute` in an entity is essential to preserve the object graph and avoid duplicate information when serializing from JSON. + +Note that specifying multiple attributes for this annotation is not currently supported. + ### `entityMapperName` From 106192915dda4e650090060318262e06ddcd2ce0 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 17:08:48 +0200 Subject: [PATCH 37/45] Document entityMapperName annotation --- Documentation/Annotations.md | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md index be996e1..3b2b00d 100644 --- a/Documentation/Annotations.md +++ b/Documentation/Annotations.md @@ -89,3 +89,43 @@ Specifying the `identityAttribute` in an entity is essential to preserve the obj Note that specifying multiple attributes for this annotation is not currently supported. ### `entityMapperName` + +If your model uses entity inheritance, use this key in the base entity to specify an entity mapper name. + +An entity mapper is used to determine which sub-entity is used when deserializing an object from JSON. + +For example, consider the following JSON: + +```json +{ + "messages": [ + { + "id": 1, + "type": "text", + "text": "Hello there!" + }, + { + "id": 2, + "type": "picture", + "image_url": "http://example.com/risitas.jpg" + } + ] +} +``` + +We could model this in Core Data using an abstract base entity `Message` and two concrete sub-entities `TextMessage` and `PictureMessage`. + +Then we need to add an `entityMapperName` entry to the `Message` entity's user info dictionary: `MessageMapper`. + +Finally we create the entity mapper and give it the name we just used: + +```objc +[NSValueTransformer grt_setEntityMapperWithName:@"MessageMapper" mapBlock:^NSString *(NSDictionary *JSONDictionary) { + NSDictionary *entityMapping = @{ + @"text": @"TextMessage", + @"picture": @"PictureMessage" + }; + NSString *type = JSONDictionary[@"type"]; + return entityMapping[type]; +}]; +``` From bbb3fa77b762bc1d268af3d4bbe884c6c12c5791 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 17:15:12 +0200 Subject: [PATCH 38/45] Update README --- README.md | 139 ++++++++++++++---------------------------------------- 1 file changed, 36 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index aa1d17a..8bbab8d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,37 @@ # Groot -With Groot you can convert JSON dictionaries and arrays to and from Core Data managed objects. +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![CocoaPods compatible](https://img.shields.io/cocoapods/v/Groot.svg) -## Requirements -Groot supports OS X 10.8+ and iOS 6.0+. +Groot provides a simple way of serializing Core Data object graphs from or into JSON. + +Groot uses the [annotations](#annotating-your-model) defined in the Core Data model to perform the serialization and provides the following features: + +1. Attribute and relationship mapping to JSON key paths. +2. [Value transformation](#value-transformers) using named `NSValueTransformer` objects. +3. [Automatic merging](#automatic-merging) +4. Support for [Entity inheritance](#entity-inheritance-support) + +## Installing Groot + +##### Using CocoaPods -## Installation -### Cocoapods Add the following to your `Podfile`: ``` ruby -pod 'Groot' +pod ‘Groot’ +``` + +Or, if you need to support iOS 6 / OS X 10.8: + +``` ruby +pod ‘Groot/ObjC’ ``` Then run `$ pod install`. -If you don't have CocoaPods installed or integrated into your project, you can learn how to do so [here](http://cocoapods.org). +If you don’t have CocoaPods installed or integrated into your project, you can learn how to do so [here](http://cocoapods.org). + +##### Using Carthage -### Carthage Add the following to your `Cartfile`: ``` @@ -27,111 +42,29 @@ Then run `$ carthage update`. Follow the instructions in [Carthage’s README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application]) to add the framework to your project. -## Usage -Suppose we would like to convert the JSON returned by a Comic Database web service into our own model objects. The JSON could look something like this: - -```json -[ - { - "id": "1699", - "name": "Batman", - "powers": [ - { - "id": "4", - "name": "Agility" - }, - { - "id": "9", - "name": "Insanely Rich" - } - ], - "publisher": { - "id": "10", - "name": "DC Comics" - }, - "real_name": "Bruce Wayne" - }, - ... -] -``` +You may need to set **Embedded Content Contains Swift Code** to **YES** in the build settings for targets that only contain Objective-C code. -We could model this in Core Data using 3 entities: Character, Power and Publisher. - -![Model](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/sample-model.jpg) - -Note that we don't need to name our attributes as in the JSON. The serialization process can be customized by adding certain information to the user dictionary provided in Core Data *entities*, *attributes* and *relationships*. - -For instance, we can specify that the `identifier` attribute will be mapped from the `id` JSON key path, and that its value will be transformed using an `NSValueTransformer` named *GRTTestTransfomer*. - -![Property User Info](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/property-userInfo.jpg) - -Now we can easily convert JSON data and insert the corresponding managed objects with a simple method call: - -```objc -NSDictionary *batmanJSON = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ - @{ - @"id": @"4", - @"name": @"Agility" - }, - @{ - @"id": @"9", - @"name": @"Insanely Rich" - }], - @"publisher": @{ - @"id": @"10", - @"name": @"DC Comics" - } -}; - -NSError *error = nil; -NSManagedObject *batman = [GRTJSONSerialization insertObjectForEntityName:@"Character" - fromJSONDictionary:batmanJSON - inManagedObjectContext:context - error:&error]; -``` +## Annotating your model -### Merging data +An exhaustive list of user info keys and values. +Reassure the reader that it works both ways. +How to ignore a property. -When inserting data, Groot does not check if the serialized managed objects already exist and simply treats them as new. +## Value transformers -If instead, you would like to merge (that is, create or update) the serialized managed objects, then you need to tell Groot how to uniquely identify your model objects. You can do that by associating the `identityAttribute` key with the name of an attribute in the *entity* user info dictionary. +## Serializing from JSON -![Entity User Info](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/entity-userInfo.jpg) +## Automatic merging -In our sample, all of our models are identified by the `identifier` attribute. +## Entity inheritance -Now we can update the Batman character we just inserted in the previous snippet: - -```objc -NSDictionary *updateJSON = @{ - @"id": @"1699", - @"real_name": @"Guille Gonzalez" -} - -// This will return the previously created managed object -NSManagedObject *batman = [GRTJSONSerialization mergeObjectForEntityName:@"Character" - fromJSONDictionary:batmanJSON - inManagedObjectContext:context - error:NULL]; -``` - -If you want to merge a JSON array, its better to call `mergeObjectsForEntityName:fromJSONArray:inManagedObjectContext:error:`. This method will perform a single fetch per entity regardless of the number of objects in the JSON array. - -### Back to JSON - -You can convert managed objects into their JSON representations by using `JSONDictionaryFromManagedObject:` or `JSONArrayFromManagedObjects:`. - -```objc -NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:someManagedObject]; -``` +## Serializing to JSON ## Contact + [Guillermo Gonzalez](http://github.com/gonzalezreal) [@gonzalezreal](https://twitter.com/gonzalezreal) ## License -Groot is available under the MIT license. See [LICENSE](https://github.com/gonzalezreal/Groot/blob/master/LICENSE). + +Groot is available under the [MIT license](LICENSE.md). \ No newline at end of file From 12d18609fdd84380c796eeade4699856e1ec2ff1 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 18:58:45 +0200 Subject: [PATCH 39/45] Update README --- README.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8bbab8d..d535664 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ Groot provides a simple way of serializing Core Data object graphs from or into JSON. -Groot uses the [annotations](#annotating-your-model) defined in the Core Data model to perform the serialization and provides the following features: +Groot uses [annotations](Documents/Annotations.md) in the Core Data model to perform the serialization and provides the following features: 1. Attribute and relationship mapping to JSON key paths. -2. [Value transformation](#value-transformers) using named `NSValueTransformer` objects. -3. [Automatic merging](#automatic-merging) -4. Support for [Entity inheritance](#entity-inheritance-support) +2. Value transformation using named `NSValueTransformer` objects. +3. Object graph preservation. +4. Support for entity inheritance ## Installing Groot @@ -44,21 +44,95 @@ Follow the instructions in [Carthage’s README](https://github.com/Carthage/Car You may need to set **Embedded Content Contains Swift Code** to **YES** in the build settings for targets that only contain Objective-C code. -## Annotating your model +## Getting started + +Consider the following JSON describing a well-known comic book character: + +```json +{ + "id": "1699", + "name": "Batman", + "real_name": "Bruce Wayne" + "powers": [ + { + "id": "4", + "name": "Agility" + }, + { + "id": "9", + "name": "Insanely Rich" + } + ], + "publisher": { + "id": "10", + "name": "DC Comics" + } +} +``` + +We could translate this into a Core Data model using three entities: `Character`, `Power` and `Publisher`. + +Model + + +### Mapping attributes and relationships + +Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as **annotations**. + +In our example, we should add a `JSONKeyPath` in the user info dictionary of each attribute and relationship specifying the corresponding key path in the JSON: + +* `id` for the `identifier` attribute, +* `name` for the `name` attribute, +* `real_name` for the `realName` attribute, +* `powers` for the `powers` relationship, +* `publisher` for the `publisher` relationship, +* etc. + +Attributes and relationships that don't have a `JSONKeyPath` entry are **not considered** for JSON serialization or deserialization. + +### Value transformers + +When we created the model we decided to use `Integer 64` for our `identifier` attributes. The problem is that, for compatibility reasons, the JSON uses strings for `id` values. + +We can add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary specifying the name of a value transformer that converts strings to numbers. + +Groot provides a simple way for creating and registering named value transformers: + +```swift +// Swift + +func toString(value: Int) -> String? { + return "\(value)" +} + +func toInt(value: String) -> Int? { + return value.toInt() +} + +NSValueTransformer.setValueTransformerWithName("StringToInteger", transform: toString, reverseTransform: toInt) +``` + +```objc +// Objective-C + +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +} reverseTransformBlock:^id(NSNumber *value) { + return [value stringValue]; +}]; +``` -An exhaustive list of user info keys and values. -Reassure the reader that it works both ways. -How to ignore a property. +### Object graph preservation -## Value transformers +To preserve the object graph and avoid duplicating information when serializing managed objects from JSON, Groot needs to know how to uniquely identify your model objects. -## Serializing from JSON +In our example, we should add an `identityAttribute` entry to the `Character`, `Power` and `Publisher` entities user dictionaries with the value `identifier`. -## Automatic merging +### Serializing from JSON -## Entity inheritance +### Entity inheritance -## Serializing to JSON +### Serializing to JSON ## Contact From 7964473f392922456b64dc5f3aa224b56544f8ae Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 19:00:01 +0200 Subject: [PATCH 40/45] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d535664..1a9e9c5 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Consider the following JSON describing a well-known comic book character: { "id": "1699", "name": "Batman", - "real_name": "Bruce Wayne" + "real_name": "Bruce Wayne", "powers": [ { "id": "4", From 9065c6525102192cbdc6cffb25bb785cd465c1f2 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 19:02:14 +0200 Subject: [PATCH 41/45] Fix Annotations link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a9e9c5..0149ff1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Groot provides a simple way of serializing Core Data object graphs from or into JSON. -Groot uses [annotations](Documents/Annotations.md) in the Core Data model to perform the serialization and provides the following features: +Groot uses [annotations](Documentation/Annotations.md) in the Core Data model to perform the serialization and provides the following features: 1. Attribute and relationship mapping to JSON key paths. 2. Value transformation using named `NSValueTransformer` objects. From c3cfc8e687bbee206acb78366c4662220ecb11c2 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 20:07:30 +0200 Subject: [PATCH 42/45] Update README --- README.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/README.md b/README.md index 0149ff1..338ba54 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,108 @@ To preserve the object graph and avoid duplicating information when serializing In our example, we should add an `identityAttribute` entry to the `Character`, `Power` and `Publisher` entities user dictionaries with the value `identifier`. +For more information about annotating your model have a look at [Annotations](Documentation/Annotations.md). + ### Serializing from JSON +Now that we have our Core Data model ready we can start adding some data. + +```swift +// Swift + +let batmanJSON: JSONObject = [ + "name": "Batman", + "id": "1699", + "powers": [ + [ + "id": "4", + "name": "Agility" + ], + [ + "id": "9", + "name": "Insanely Rich" + ] + ], + "publisher": [ + "id": "10", + "name": "DC Comics" + ] +] + +let batman: Character = objectFromJSONDictionary(batmanJSON, + inContext: context, mergeChanges: false, error: &error) +``` + +```objc +// Objective-C + +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; +``` + +If we want to update the object we just created, Groot can merge the changes for us: + +```objc +// Objective-C + +NSDictionary *updateJSON = @{ + @"id": @"1699", + @"real_name": @"Bruce Wayne", +}; + +// This will return the previously created managed object +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:updateJSON inContext:self.context error:&error]; +``` + +#### Serializing relationships from identifiers + +Suppose that our API does not return full objects for the relationships but only the identifiers. + +We don't need to change our model to support this situation: + +```objc +// Objective-C + +NSDictionary *batmanJSON = @{ + @"name": @"Batman", + @"real_name": @"Bruce Wayne", + @"id": @"1699", + @"powers": @[@"4", @"9"], + @"publisher": @"10" +}; + +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; +``` + +The above code creates a full `Character` object and the corresponding relationships pointing to `Power` and `Publisher` objects that just have the identifier attribute populated. + +We can import powers and publisher from different JSON objects and Groot will merge them nicely: + +```objc +// Objective-C + +NSArray *powersJSON = @[ + @{ + @"id": @"4", + @"name": @"Agility" + }, + @{ + @"id": @"9", + @"name": @"Insanely Rich" + } +]; + +[GRTJSONSerialization objectsWithEntityName:@"Power" fromJSONArray:powersJSON inContext:self.context error:&error]; + +NSDictionary *publisherJSON = @{ + @"id": @"10", + @"name": @"DC Comics" +}; + +[GRTJSONSerialization objectWithEntityName:@"Publisher" fromJSONDictionary:publisherJSON inContext:self.context error:&error]; +``` + +For more serialization options check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). + ### Entity inheritance ### Serializing to JSON From 1245a2c9f277de8db52592a2419bea59ad57f912 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 20:18:19 +0200 Subject: [PATCH 43/45] Update README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 338ba54..4899a4b 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ We could translate this into a Core Data model using three entities: `Character` ### Mapping attributes and relationships -Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as **annotations**. +Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as [annotations](Documentation/Annotations.md). In our example, we should add a `JSONKeyPath` in the user info dictionary of each attribute and relationship specifying the corresponding key path in the JSON: @@ -232,6 +232,10 @@ For more serialization options check [GRTJSONSerialization.h](Groot/GRTJSONSeria ### Entity inheritance +Groot supports entity inheritance via the [entityMapperName](Documentation/Annotations.md#entityMapperName) annotation. + +If you are using SQLite as your persistent store, Core Data implements entity inheritance by creating one table for the parent entity and all child entities, with a superset of all their attributes. This can obviously have unintended performance consequences if you have a lot of data in the entities, so use this feature wisely. + ### Serializing to JSON ## Contact From 204c51e334b5e69c8bfdde23adbcbcf9184efb35 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 20:20:17 +0200 Subject: [PATCH 44/45] Fix link to entityMapperName documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4899a4b..65b3af3 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ For more serialization options check [GRTJSONSerialization.h](Groot/GRTJSONSeria ### Entity inheritance -Groot supports entity inheritance via the [entityMapperName](Documentation/Annotations.md#entityMapperName) annotation. +Groot supports entity inheritance via the [entityMapperName](Documentation/Annotations.md#entitymappername) annotation. If you are using SQLite as your persistent store, Core Data implements entity inheritance by creating one table for the parent entity and all child entities, with a superset of all their attributes. This can obviously have unintended performance consequences if you have a lot of data in the entities, so use this feature wisely. From 70bb25111d25256cc70fd855c1fbe4f062ee7d44 Mon Sep 17 00:00:00 2001 From: Guillermo Gonzalez Date: Sun, 19 Jul 2015 20:38:00 +0200 Subject: [PATCH 45/45] Update README --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65b3af3..c12ae0c 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ NSDictionary *publisherJSON = @{ [GRTJSONSerialization objectWithEntityName:@"Publisher" fromJSONDictionary:publisherJSON inContext:self.context error:&error]; ``` -For more serialization options check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). +For more serialization methods check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). ### Entity inheritance @@ -238,6 +238,22 @@ If you are using SQLite as your persistent store, Core Data implements entity in ### Serializing to JSON +Groot provides methods to serialize managed objects back to JSON: + +```swift +// Swift + +let JSONDictionary = JSONDictionaryFromObject(batman) +``` + +```objc +// Objective-C + +NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromObject:batman]; +``` + +For more serialization methods check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). + ## Contact [Guillermo Gonzalez](http://github.com/gonzalezreal)