Skip to content

Instantly share code, notes, and snippets.

@matzew
Last active December 10, 2015 15:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save matzew/843c89668b2216929633 to your computer and use it in GitHub Desktop.
Save matzew/843c89668b2216929633 to your computer and use it in GitHub Desktop.
AeroGear and CoreData - Entity Mapper

AeroGear and CoreData: AGEntityMapper

##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

The actual code

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];
@hborders
Copy link

hborders commented Jan 6, 2013

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.

AGEntityMapper *taskMapper = [[AGEntityMapper alloc] initWithClass:[Task class]];
[taskMapper addMappingFromKey:@"description" toSetterSelector:@selector(setDesc)];

@chriseidhof
Copy link

It would be nice to wrap the observing inside a separate method so you don't have to worry about it...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment