##Background
Mapping JSON representations to a "rich" domain model in ObjC is a bit cumbersome. Similar is true when mapping a JSON data/response to a NSMangedObject (and vise versa). There is a base framework for "remote persistence", via the CoreData API, by leveraging a custom NSIncrementalStore -> AFIncrementalStore.
For that a similar two-way-mapping is required, as described here.
##AeroGear and CoreData
Our CoreData offerings are leveraging the AFIncrementalStore, therefore (as with other frameworks/libraries) we need a mapping as well. We need to know the name of the entity and we need a NSDictionary
that covers the actual JSON/property mapping (see above links/blog posts for details). One (awful) option could be building the schema with the vanilla ObjC classes:
// some mappers:
NSDictionary *task_mapper =
[NSDictionary dictionaryWithObjectsAndKeys:@"description",@"desc",@"id",@"myId", nil];
NSDictionary *project_mapper =
[NSDictionary dictionaryWithObjectsAndKeys:@"id",@"myId", nil];
// create a schema out of the mappers:
NSDictionary *schema =
[NSDictionary dictionaryWithObjectsAndKeys:task_mapper, @"Task", project_mapper, @"Project", nil];
// pass the schema to the AGCoreDataHelper class, when doing the init...
Per entity a NSDictionary
represents the mapping between the external representation (JSON) and the actual (managed) object. All mappings are now stored in another NSDictionary
, where the entity class name is the key for the actual mapping. This would be pretty awful...
##Proposal for a custom API
I'd like to introduce a new wrapper type API, called AGEntityMapper
:
@interface AGEntityMapper : NSObject
@property NSString *name;
@property NSDictionary *mapper;
-(id) initForEntity:(NSString *) entityName mapper:(NSDictionary *) mapper;
@end
The AGEntityMapper
has a name, for the entity class, and a mapping (externalRep <-> JSON). When setting up the AeroGear CoreData client (AGCoreDataConfig
) you simply apply all AGEntityMapper
objects:
@protocol AGCoreDataConfig <NSObject>
@property (strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (strong, nonatomic) NSURL *baseURL;
-(void)applyEntityMappers:(AGEntityMapper *)firstObject, ... NS_REQUIRES_NIL_TERMINATION;
@end
Here is a simple model:
@interface Task : NSManagedObject
@property (nonatomic, retain) NSString * title;
// The 'desc' property, will contain the value
// of the 'description' key from a JSON response.
@property (nonatomic, retain) NSString * desc;
@end
Code that issues a HTTP GET (fetch) request for some Task objects:
...
// location to MOM (inside of a unit test)
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSURL *url = [bundle URLForResource:@"TestModel" withExtension:@"momd"];
NSManagedObjectModel* managedObjectModel =
[[NSManagedObjectModel alloc] initWithContentsOfURL:url];
// set up the entity mapper... to map JSON keys, to managed object properties
AGEntityMapper *taskMapper =
[[AGEntityMapper alloc] initForEntity:@"Task"
// mapping the properties on the "entity" (NSManagedObject)
// to the external representation (e.g. JSON)
mapper:@{ @"desc": @"description"}];
// create the "AeroGear CoreData Helper"
AGCoreDataHelper *helper =
[[AGCoreDataHelper alloc] initWithConfig:^(id<AGCoreDataConfig> config) {
[config setManagedObjectModel:managedObjectModel];
[config setBaseURL:
[NSURL URLWithString:@"https://todoauth-aerogear.rhcloud.com/todo-server/"]];
[config setAuthMod:myAeroGearAuthMod];
[config applyEntityMappers:taskMapper, nil];
}];
// get the MOC (from the "helper"), to work with CoreData
NSManagedObjectContext *context = helper.managedObjectContext;
// below is all standard CoreData API
// a regular fetch request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Task"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
NSError *error = nil;
// in this demo, we are adding an inline block/callback
// invoked, once the fetch has been completed
[[NSNotificationCenter defaultCenter]
addObserverForName:AFIncrementalStoreContextDidFetchRemoteValues
object:context
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
NSDictionary *userInfo = [note userInfo];
// HTTP GET result:
NSArray *fetchedObjects =
[userInfo objectForKey:AFIncrementalStoreFetchedObjectsKey];
for(Task *task in fetchedObjects) {
NSLog(@"Task; title: %@; desc: %@", task.title, task.desc);
}
_finishedFlag = YES;
}];
// issue the actual fetch
// (--> HTTP GET against _https://todoauth-aerogear.rhcloud.com/todo-server/tasks_)
[context executeFetchRequest:fetchRequest error:&error];
If you're going to create a separate object for the mapping data, why not specify types for the key names instead of using an NSDictionary? Also, I prefer to work in terms of selectors so that you get a slight bit of help from the compiler. Thus, use strings for json keys and selectors for setters to call. Unfortunately, the syntax wouldn't be as nice as what you have. I'm willing to sacrifice compile-time safety for syntax, but you may not be.