Skip to content

Instantly share code, notes, and snippets.

@evadne
Created February 3, 2011 04:48
Show Gist options
  • Save evadne/809061 to your computer and use it in GitHub Desktop.
Save evadne/809061 to your computer and use it in GitHub Desktop.
//
// IRManagedObject.h
// Milk
//
// Created by Evadne Wu on 1/11/11.
// Copyright 2011 Iridia Productions. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "IRFoundationAdditions.h"
#import "IRNoOp.h"
@interface IRManagedObject : NSManagedObject
// Notes:
// Remember that you’ll have to also update your data model, so the custom class is used on your entity
+ (NSArray *) insertOrUpdateObjectsIntoContext:(NSManagedObjectContext *)inContext withExistingProperty:(NSString *)inLocalMarkerKeyPath matchingKeyPath:(NSString *)inRemoteMarkerKeyPath ofRemoteDictionaries:(NSArray *)inRemoteDictionaries;
// Like INSERT … ON DUPLICATE KEY UPDATE. If existing value at key path matches the remote key path, the existing object and the remote representation dictionary is thought as two different snapshots of a logically single object, whose properties will be updated using the remote representation.
// The returned array contains one object per remote representation. Some of them could be old but updated objects, other being newly inserted objects. The idea is that you’ll fix relationships using the returned array, if necessary.
// The order of the returned array is guaranteed to be of the same order as the incoming remote dictionaries are ordered.
// The values that the local and remote key paths point to should always respond to -compare:.
+ (NSArray *) insertOrUpdateObjectsUsingContext:(NSManagedObjectContext *)inContext withRemoteResponse:(NSArray *)inRemoteDictionaries usingMapping:(NSDictionary *)inRemoteKeyPathsToIRManagedObjectSubclasses options:(int)aBitMask;
// This is a higher-level wrapper dealing with circumstances where an object representation contains other representations that are also of interest to the developer.
// For example:
// { id: 20, related: { id: 201, title: "Hi" }, title: "Ho" }
//
// Where the “related” object is a representation that another IRManagedObject subclass understands
// This method can potentially generate a lot of I/O.
// Note, that “related” is an object here. This method also works when the node is an collection.
// Collection traversal is implemented using fast enumeration.
// The order of the collection is not respected.
// Example:
//
// [MLGoogleReaderFeed insertOrUpdateObjectsUsingContext:[[self newManagedObjectContext] autorelease] withRemoteResponse:[inResponseOrNil objectForKey:@"subscriptions"] usingMapping:[NSDictionary dictionaryWithObjectsAndKeys:
//
// [MLGoogleReaderUser class], @"user",
// [MLGoogleReaderFeed class], @"labels",
//
// nil] options:nil];
// N.B. that usingMapping: takes a dictionary, whose keys are remote key paths.
// For example, if your subclass mirrors “id” to “identifier”, the dictionary keys the class to “id”.
// By default, if the representation is found to represent an existing object, and that object has to-many relationships, all the previous related objects are replaced by the new ones. Pass kIRManagedObjectMergeRelatedObjects as a bit-mask to options, so the new objects are added to the relationship but the old ones are retained.
+ (NSString *) coreDataEntityName;
// Default returns NSStringFromClass([self class]);
+ (NSString *) keyPathHoldingUniqueValue;
// This method defaults to nil.
// Return a NSString, so -insertOrUpdateObjectsUsingContext:withRemoteResponse:usingMapping: works.
+ (id) objectInsertingIntoContext:(NSManagedObjectContext *)inContext withRemoteDictionary:(NSDictionary *)inDictionary;
// Makes a new object, inserting into the context, then call its -configureWithRemoteDictionary:.
@end
@interface IRManagedObject (WebAPIImporting)
- (void) configureWithRemoteDictionary:(NSDictionary *)inDictionary;
// Takes values from a remote dictionary. Does not whatsoever change the contents.
// If +remoteDictionaryConfigurationMapping is implemented, does not return nil, uses the mapping.
// Otherwise, default implementation does nothing.
+ (NSDictionary *) remoteDictionaryConfigurationMapping;
// To avoid rolling your own -valueForKeyPath wrappers that avoids [NSNull null] et al, implement +remoteDictionaryConfigurationMapping, whose keys are remote dictionary key path strings, and their values the key path string to the desired value of the local object, that the remote value goes into.
// If the returned local key path is actually [NSNull null], the value gets ignored.
// Consider making this method return a static object if necessary.
+ (id) transformedValue:(id)aValue fromRemoteKeyPath:(NSString *)aRemoteKeyPath toLocalKeyPath:(NSString *)aLocalKeyPath;
// Returns a transformed, if any, or nil, for value at a particular key path.
// This allows the subclass to do custom value transformation.
// Placeholders are also transformed.
// Defaults to the incoming value. Return [IRNoOp noOp] to do nothing.
// This is not a replacement for overriding -set<Property>: and implementing custom transformations before calling -setPrimitive<Property>:. This is for use when the implementations of IRManagedObject+WebAPIImporting is provided in file for another class.
+ (id<NSObject>) placeholderForNonexistantKey;
// Placeholder value to use if a remote value that is specified within the mapping is not found.
// e.g., if a key “bogus” is specified in the mapping, but the incoming dictionary does not have it.
// Defaults to nil.
// Try [IRNoOp noOp] if you want to skip the key instead of niling or setting the value to [NSNull null]. It is useful to have a no-op, if you will touch the object more than once, and some incoming data is incomplete.
+ (id<NSObject>) placeholderForNullValue;
// Placeholder value to use if a remote value is [NSNull null].
// Defaults to nil.
// Try [IRNoOp noOp] if you want to skip the key instead of niling or setting the value to [NSNull null].
@end
//
// IRManagedObject.m
// Milk
//
// Created by Evadne Wu on 1/11/11.
// Copyright 2011 Iridia Productions. All rights reserved.
//
#import "IRManagedObject.h"
@implementation IRManagedObject
+ (NSArray *) insertOrUpdateObjectsIntoContext:(NSManagedObjectContext *)context withExistingProperty:(NSString *)managedObjectKeyPath matchingKeyPath:(NSString *)dictionaryKeyPath ofRemoteDictionaries:(NSArray *)dictionaries {
// The value that local or remote key paths point to will be called markers
if (!dictionaries || [dictionaries isEqual:[NSNull null]] || ([dictionaries count] == 0))
return nil;
if (!managedObjectKeyPath || !dictionaryKeyPath)
return nil;
NSError *error = nil;
NSArray *existingEntities = [context executeFetchRequest:(( ^ {
NSFetchRequest *returnedRequest = [[[NSFetchRequest alloc] init] autorelease];
[returnedRequest setEntity:[NSEntityDescription entityForName:[self coreDataEntityName] inManagedObjectContext:context]];
[returnedRequest setPredicate:[NSPredicate predicateWithFormat:
@"(%K IN %@)",
managedObjectKeyPath,
[[dictionaries irMap:irMapMakeWithKeyPath(dictionaryKeyPath)] irMap:irMapNullFilterMake()]
]];
[returnedRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:managedObjectKeyPath ascending:YES]]];
return returnedRequest;
})()) error:&error];
if (!existingEntities)
return nil;
NSUInteger existingEntitiesCount = [existingEntities count];
__block NSUInteger currentEntityIndex = -1;
IRManagedObject *currentEntity = (existingEntitiesCount > 0) ? [existingEntities objectAtIndex:0] : nil;
id (^nextEntity)() = ^ {
if (!currentEntity)
return (id)nil;
currentEntityIndex++;
if (currentEntityIndex == existingEntitiesCount)
return (id)nil;
return (id)[existingEntities objectAtIndex:currentEntityIndex];
};
NSComparisonResult (^compare) (id, id) = ^ (id inEntity, id inRemoteDictionary) {
return [[inEntity valueForKey:managedObjectKeyPath] compare:[inRemoteDictionary valueForKeyPath:dictionaryKeyPath]];
};
NSMutableArray *returnedEntities = [[dictionaries mutableCopy] autorelease];
NSArray *sortedRemoteDictionaries = [dictionaries sortedArrayUsingComparator:irComparatorMakeWithNodeKeyPath(dictionaryKeyPath)];
for (id currentDictionary in sortedRemoteDictionaries) {
if ([currentDictionary isEqual:[NSNull null]])
continue;
// When the dictionary has a marker that is ahead of the entity, move on to the next entity
if (currentEntity)
while (compare(currentEntity, currentDictionary) == NSOrderedAscending) {
currentEntity = nextEntity();
if (!currentEntity)
break;
}
// The marker of the dictionary is guaranteed to match, or fall behind the current entity
IRManagedObject *touchedEntity = nil;
if ((currentEntity != nil) && (compare(currentEntity, currentDictionary) == NSOrderedSame)) {
touchedEntity = currentEntity;
[touchedEntity configureWithRemoteDictionary:currentDictionary];
} else {
touchedEntity = [self objectInsertingIntoContext:context withRemoteDictionary:currentDictionary];
}
[returnedEntities replaceObjectAtIndex:[returnedEntities indexOfObject:currentDictionary] withObject:touchedEntity];
}
return returnedEntities;
}
+ (NSArray *) insertOrUpdateObjectsUsingContext:(NSManagedObjectContext *)inContext withRemoteResponse:(NSArray *)inRemoteDictionaries usingMapping:(NSDictionary *)inRemoteKeyPathsToIRManagedObjectSubclasses options:(int)aBitMask {
[[inContext retain] autorelease];
[[inRemoteDictionaries retain] autorelease];
[[inRemoteKeyPathsToIRManagedObjectSubclasses retain] autorelease];
NSString *localKeyPath = [self keyPathHoldingUniqueValue];
NSString *remoteKeyPath = [[[self remoteDictionaryConfigurationMapping] allKeysForObject:localKeyPath] objectAtIndex:0];
if (!inRemoteDictionaries)
return [NSArray array];
NSArray *baseEntities = [self insertOrUpdateObjectsIntoContext:inContext withExistingProperty:localKeyPath matchingKeyPath:remoteKeyPath ofRemoteDictionaries:inRemoteDictionaries];
NSDictionary *baseEntityRelationships = [[[[[inContext persistentStoreCoordinator] managedObjectModel] entitiesByName] objectForKey:[self coreDataEntityName]] relationshipsByName];
for (NSString *rootRemoteKeyPath in inRemoteKeyPathsToIRManagedObjectSubclasses) {
NSString *rootLocalKeyPath = [[self remoteDictionaryConfigurationMapping] objectForKey:rootRemoteKeyPath];
Class aSubclass = NSClassFromString([inRemoteKeyPathsToIRManagedObjectSubclasses objectForKey:rootRemoteKeyPath]);
NSString *nodeLocalKeyPath = [aSubclass keyPathHoldingUniqueValue];
NSString *nodeRemoteKeyPath = [[[aSubclass remoteDictionaryConfigurationMapping] allKeysForObject:nodeLocalKeyPath] objectAtIndex:0];
NSArray *objectOrObjectCollectionRepresentations = [inRemoteDictionaries irMap: ^ (id inObject, int index, BOOL *stop) {
id returnedObject = [inObject valueForKeyPath:rootRemoteKeyPath];
return returnedObject ? returnedObject : [NSNull null];
}];
NSMutableArray *objectRepresentations = [NSMutableArray array];
NSMutableArray *objectOrObjectCollectionRepresentationCounts = [NSMutableArray array];
for (id anObjectOrCollection in objectOrObjectCollectionRepresentations) {
if ([anObjectOrCollection isEqual:[NSNull null]]) {
[objectOrObjectCollectionRepresentationCounts addObject:[NSNumber numberWithInt:0]];
} if ([anObjectOrCollection isKindOfClass:[NSArray class]]) {
[objectRepresentations addObjectsFromArray:anObjectOrCollection];
[objectOrObjectCollectionRepresentationCounts addObject:[NSNumber numberWithInt:[(NSArray *)anObjectOrCollection count]]];
} else {
[objectRepresentations addObject:anObjectOrCollection];
[objectOrObjectCollectionRepresentationCounts addObject:[NSNumber numberWithInt:1]];
}
}
NSArray *nodeEntities = [aSubclass insertOrUpdateObjectsIntoContext:inContext withExistingProperty:nodeLocalKeyPath matchingKeyPath:nodeRemoteKeyPath ofRemoteDictionaries:objectRepresentations];
__block NSUInteger consumedNodeEntities = 0;
[objectOrObjectCollectionRepresentationCounts enumerateObjectsUsingBlock: ^ (NSNumber *aCount, NSUInteger idx, BOOL *stop) {
NSUInteger countValue = [aCount intValue];
if (countValue == 0)
return;
NSArray *objects = [nodeEntities subarrayWithRange:NSMakeRange(consumedNodeEntities, countValue)];
if ([[baseEntityRelationships objectForKey:rootLocalKeyPath] isToMany]) {
[[[baseEntities objectAtIndex:idx] mutableSetValueForKeyPath:rootLocalKeyPath] addObjectsFromArray:objects];
} else {
[[baseEntities objectAtIndex:idx] setValue:[objects objectAtIndex:0] forKeyPath:rootLocalKeyPath];
}
consumedNodeEntities += countValue;
}];
}
return baseEntities;
}
+ (NSString *) keyPathHoldingUniqueValue {
return nil;
}
+ (NSString *) coreDataEntityName {
return NSStringFromClass([self class]);
}
+ (id) objectInsertingIntoContext:(NSManagedObjectContext *)inContext withRemoteDictionary:(NSDictionary *)inDictionary {
IRManagedObject *returnedStatus = nil;
@try {
returnedStatus = [[[self alloc] initWithEntity:[NSEntityDescription entityForName:[self coreDataEntityName] inManagedObjectContext:inContext] insertIntoManagedObjectContext:inContext] autorelease];
} @catch (NSException *e) {
NSLog(@"Exception: %@", e);
}
if (!returnedStatus)
return nil;
[returnedStatus configureWithRemoteDictionary:inDictionary];
return returnedStatus;
}
@end
@interface IRManagedObject (WebAPIImporting_Private)
+ (BOOL) skipsNonexistantRemoteKey;
+ (BOOL) skipsNullValue;
@end
@implementation IRManagedObject (WebAPIImporting_Private)
+ (BOOL) skipsNonexistantRemoteKey {
return [[self placeholderForNonexistantKey] isEqual:[IRNoOp noOp]];
}
+ (BOOL) skipsNullValue {
return [[self placeholderForNullValue] isEqual:[IRNoOp noOp]];
}
@end
@implementation IRManagedObject (WebAPIImporting)
+ (NSDictionary *) remoteDictionaryConfigurationMapping {
return nil;
}
+ (id) transformedValue:(id)aValue fromRemoteKeyPath:(NSString *)aRemoteKeyPath toLocalKeyPath:(NSString *)aLocalKeyPath {
return aValue;
}
+ (id<NSObject>) placeholderForNonexistantKey {
return nil;
}
+ (id<NSObject>) placeholderForNullValue {
return nil;
}
- (void) configureWithRemoteDictionary:(NSDictionary *)inDictionary {
NSDictionary *configurationMapping = [[self class] remoteDictionaryConfigurationMapping];
if (!configurationMapping)
return;
NSAssert([configurationMapping isKindOfClass:[NSDictionary class]], @"-configureWithDictionary found +remoteDictionaryConfigurationMapping, unfortunately -isKindOfClass: disagrees with its type.");
BOOL skipsNonexistantRemoteKey = [[self class] skipsNonexistantRemoteKey];
id nonexistantRemoteKeyPlaceholder = [[self class] placeholderForNonexistantKey];
BOOL skipsNullValue = [[self class] skipsNullValue];
id nullValuePlaceholder = [[self class] placeholderForNullValue];
for (id aRemoteKeyPath in configurationMapping) {
id aRemoteValueOrNil = [inDictionary valueForKeyPath:aRemoteKeyPath];
id aLocalKeyPathOrNSNull = [configurationMapping objectForKey:aRemoteKeyPath];
if ([aLocalKeyPathOrNSNull isEqual:[NSNull null]])
continue;
NSAssert([aLocalKeyPathOrNSNull isKindOfClass:[NSString class]], @"in +remoteDictionaryConfigurationMapping, the local key path must be a NSString, or [NSNull null].");
NSString *aLocalKeyPath = (NSString *)aLocalKeyPathOrNSNull;
id committedValue = aRemoteValueOrNil;
if (!aRemoteValueOrNil) {
if (skipsNonexistantRemoteKey)
continue;
committedValue = nonexistantRemoteKeyPlaceholder;
} else if ([aRemoteValueOrNil isEqual:[NSNull null]]) {
if (skipsNullValue)
continue;
committedValue = nullValuePlaceholder;
}
// If the committed value is actually an array we assume that it’ll be taken care of by insertOrUpdateObjectsUsingContext:withRemoteResponse:usingMapping:options: instead
if (![committedValue isKindOfClass:[NSArray class]])
[self setValue:[[self class] transformedValue:committedValue fromRemoteKeyPath:aRemoteKeyPath toLocalKeyPath:aLocalKeyPath] forKeyPath:aLocalKeyPath];
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment