Skip to content

Commit

Permalink
Merge branch 'master' of github.com:mutualmobile/MMRecord into cnstol…
Browse files Browse the repository at this point in the history
…l_tweaks
  • Loading branch information
Conrad Stoll committed Jun 24, 2014
2 parents 3dabfb5 + c72549d commit 91e1435
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@

@interface MMFoursquareSessionManager : AFHTTPSessionManager

/**
A shared instance of the AFHTTPSessionManager configured for the foursquare API. This instance of
the session manager is intended to be used as an example for the AFMMRecordResponseSerializer, and
will be configured with that response serializer such that it returns MMRecord subclasses in the
response object.
*/
+ (instancetype)sharedClient;

/**
A shared instance of the AFHTTPSessionManager configured for the foursquare API. This instance of
the session manager is intended to be used as an example for the AFMMRecordSessionManagerServer,
and will be configured normally to return a standard dictionary/array response object such that
it will function normally for the session manager server MNMRecord example.
*/
+ (instancetype)serverClient;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@

@implementation MMAppDelegate

/*
This appDelegate initialization implementation is customized to set up two different MMRecord
stack configurations. This example uses MMRecord both in the traditional way, with a subclass of
MMServer that gets used to make requests via MMRecord's request entry points. It also implements
an example using the AFMMRecordResponseSerializer, where requests are made through an
AFNetworking 2.0 session manager.
*/
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// AFMMRecordSessionManagerServer Example
MMFoursquareSessionManager *serverClientManager = [MMFoursquareSessionManager serverClient];

[AFMMRecordSessionManagerServer registerAFHTTPSessionManager:serverClientManager];
[FSRecord registerServerClass:[AFMMRecordSessionManagerServer class]];

// AFMMRecordResponseSerializer Example
MMFoursquareSessionManager *sessionManager = [MMFoursquareSessionManager sharedClient];

NSManagedObjectContext *context = [[MMDataManager sharedDataManager] managedObjectContext];
Expand Down
25 changes: 25 additions & 0 deletions Source/AFMMRecordResponseSerializer/AFMMRecordResponseSerializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ extern NSString * const AFMMRecordResponseSerializerWithDataKey;

@protocol AFMMRecordResponseSerializationEntityMapping;

@class MMRecordOptions;

/**
AFMMRecordResponseSerializer is a response serializer for AFNetworking that can serialize an HTTP
response object into a list of MMRecord objects. MMRecord objects are subclasses of Core Data
Expand Down Expand Up @@ -72,6 +74,29 @@ extern NSString * const AFMMRecordResponseSerializerWithDataKey;
responseObjectSerializer:(AFHTTPResponseSerializer *)serializer
entityMapper:(id<AFMMRecordResponseSerializationEntityMapping>)mapper;

/**
This method allows you to set a specific set of options for the MMRecord response serializer.
Unlike the MMRecord setOptions method, where the options are implicitly applied only to the request
following the invocation of setOptions, this method will apply the referenced options to all
requests made by this class.
By default the response serializer will use the default options for the initial entity's MMRecord
subclass. This means that attributes like the keyPathForResponseObject will be applied correctly,
but also means that subclassing the MMRecord defaultOptions method for various record classes is
indeed possible. With great power comes greater responsibility - use this wisely!
@param options The options object to be set on AFMMRecordResponseSerializer.
@discussion If no options are set then the default MMRecord options are applied to the requests.
@warning Passing nil will restore the default MMRecord options.
*/
+ (void)registerOptions:(MMRecordOptions *)options;

/**
Returns the current set of options for the response serializer. If no options have been set this
method will return nil.
*/
+ (MMRecordOptions *)currentOptions;

@end


Expand Down
41 changes: 34 additions & 7 deletions Source/AFMMRecordResponseSerializer/AFMMRecordResponseSerializer.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#import "MMRecord.h"
#import "MMRecordResponse.h"

static MMRecordOptions* MM_responseSerializerRecordOptions;

NSString * const AFMMRecordResponseSerializerWithDataKey = @"AFMMRecordResponseSerializerWithDataKey";

@interface AFMMRecordResponseSerializer ()
Expand All @@ -51,15 +53,33 @@ + (instancetype)serializerWithManagedObjectContext:(NSManagedObjectContext *)con
return serializer;
}

+ (void)registerOptions:(MMRecordOptions *)options {
MM_responseSerializerRecordOptions = options;
}

+ (MMRecordOptions *)currentOptions {
if (MM_responseSerializerRecordOptions != nil) {
return MM_responseSerializerRecordOptions;
}

return nil;
}


#pragma mark - Private

- (NSArray *)responseArrayFromResponseObject:(id)responseObject
initialEntity:(NSEntityDescription *)initialEntity {
Class managedObjectClass = NSClassFromString([initialEntity managedObjectClassName]);
+ (MMRecordOptions *)currentOptionsWithMMRecordSubclass:(Class)recordClass {
if (MM_responseSerializerRecordOptions != nil) {
return MM_responseSerializerRecordOptions;
}

NSString *keyPathForResponseObject = [managedObjectClass keyPathForResponseObject];
MMRecordOptions *options = [recordClass defaultOptions];

return options;
}

- (NSArray *)responseArrayFromResponseObject:(id)responseObject
keyPathForResponseObject:(NSString *)keyPathForResponseObject {
id recordResponseObject = responseObject;

if (keyPathForResponseObject != nil) {
Expand Down Expand Up @@ -146,14 +166,21 @@ - (id)responseObjectForResponse:(NSURLResponse *)response
responseObject:responseObject
context:self.context];

Class managedObjectClass = NSClassFromString([initialEntity managedObjectClassName]);

MMRecordOptions *options = [AFMMRecordResponseSerializer currentOptionsWithMMRecordSubclass:managedObjectClass];

NSString *keyPathForResponseObject = [options keyPathForResponseObject];

NSArray *responseArray = [self responseArrayFromResponseObject:responseObject
initialEntity:initialEntity];
keyPathForResponseObject:keyPathForResponseObject];

NSManagedObjectContext *backgroundContext = [self backgroundContext];

MMRecordResponse *recordResponse = [MMRecordResponse responseFromResponseObjectArray:responseArray
initialEntity:initialEntity
context:self.context];
initialEntity:initialEntity
context:self.context
options:options];

NSArray *records = [self recordsFromMMRecordResponse:recordResponse
backgroundContext:backgroundContext];
Expand Down
6 changes: 2 additions & 4 deletions Source/MMRecord/MMRecord.m
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,8 @@ + (NSArray*)recordsFromResponseObject:(id)responseObject
}
MMRecordResponse *response = [MMRecordResponse responseFromResponseObjectArray:recordResponseArray
initialEntity:initialEntity
context:context];

response.entityPrimaryKeyInjectionBlock = options.entityPrimaryKeyInjectionBlock;
response.recordPrePopulationBlock = options.recordPrePopulationBlock;
context:context
options:options];

NSArray *records = [response records];

Expand Down
30 changes: 27 additions & 3 deletions Source/MMRecord/MMRecordMarshaler.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,34 @@
+ (void)establishPrimaryKeyRelationshipFromProtoRecord:(MMRecordProtoRecord *)protoRecord
toParentRelationshipPrimaryKeyProtoRecord:(MMRecordProtoRecord *)parentRelationshipPrimaryKeyProto;

/**
This method is used when a response contains multiple instances of the same record. MMRecord will
always do its best to create and populate these records and associate them with the appropriate
relationships on various other records. Generally speaking, its expected that a response will
contain either duplicate references to record primary keys, which allow MMRecord to fetch the
appropriate record to associate as a relationship. Or, the response may contain duplicate fully
saturated objects. In this case, the first object will "win", and all other references to that
object will be populated using the first one that is found.
This method is intended as a means to merge those various different response objects together to
create a master instance of a particular record. In some responses an object may contain a subset
of data in one place, and a larger subset of data in another place. In those instances, a user
may want for the larger subset to win, even if it wasn't found by MMRecord first.
You may override this method in a subclass of MMRecordMarshaler to provide this functionality.
@param dictionary The dictionary for the n+1th record response object of a given type and
primary key.
@param protoRecord The proto record created to represent this specific object by MMRecord.
@discussion This method has no default implementation. You must subclass MMRecordMarshaler to
provide your own implementation.
*/
+ (void)mergeDuplicateRecordResponseObjectDictionary:(NSDictionary *)dictionary
withExistingProtoRecord:(MMRecordProtoRecord *)protoRecord;

///-------------------------------
/// @name Public Interface Methods
///-------------------------------

///---------------------------------
/// @name Public Subclassing Methods
///---------------------------------

/**
This method is designed to be subclassed. It should be called to handle the population of a
Expand Down
6 changes: 6 additions & 0 deletions Source/MMRecord/MMRecordMarshaler.m
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ + (void)establishPrimaryKeyRelationshipFromProtoRecord:(MMRecordProtoRecord *)pr
}
}

+ (void)mergeDuplicateRecordResponseObjectDictionary:(NSDictionary *)dictionary
withExistingProtoRecord:(MMRecordProtoRecord *)protoRecord {
// There is no default implementation for this method. Feel free to provide your own :)
}


#pragma mark - To Many Relationship Test

// TODO: Simplify this method by refactor/extract
Expand Down
14 changes: 14 additions & 0 deletions Source/MMRecord/MMRecordRepresentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@
*/
- (NSString *)primaryKeyPropertyName;

/**
This method is responsible for determining the name of the primary key property given the entity
description for this instance of an MMRecordRepresentation.
@param entity The entity description for this representation.
@return The name of the primary key property for this entity.
@discussion This method is designed to be subclassed if the user wishes to provide a different way
of determining which property to use as the primary key for a given type of entity.
@warning This method is called once per entity or superentity until a primary key is found, or no
further superentity exists for a given entity. It is in this way that this class supports entity
inheritance, meaning that if no primary key property is designated on an entity then the class will
go through the chain of superentities to see if a primary key exists there.
*/
- (NSString *)primaryKeyPropertyNameForEntityDescription:(NSEntityDescription *)entity;

/**
This method returns the primary key attribute description for this entity. This method will return
nil if the entity uses a relationship for its primary key.
Expand Down
36 changes: 22 additions & 14 deletions Source/MMRecord/MMRecordRepresentation.m
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ - (instancetype)initWithEntity:(NSEntityDescription *)entity {
_attributeRepresentations = [NSMutableArray array];
_relationshipRepresentations = [NSMutableArray array];
_recordClassDateFormatter = [NSClassFromString([entity managedObjectClassName]) dateFormatter];
_primaryKey = [self primaryAttributeKeyForEntityDescription: entity];
_primaryKey = [self representationPrimaryKeyForEntityDescription:entity];
//TODO: Improve error handling for invalid primary key that may be returned from a subclass
// or be misconfigured in the model file.

[self createRepresentationMapping];
}
Expand Down Expand Up @@ -116,6 +118,25 @@ - (NSString *)primaryKeyPropertyName {
return primaryKey;
}

- (NSString *)primaryKeyPropertyNameForEntityDescription:(NSEntityDescription *)entity {
NSDictionary *userInfo = [entity userInfo];
NSString *primaryKeyPropertyName = [userInfo valueForKey:MMRecordEntityPrimaryAttributeKey];

return primaryKeyPropertyName;
}

- (NSString *)representationPrimaryKeyForEntityDescription:(NSEntityDescription *)entity {
NSString *primaryKeyPropertyName;
NSEntityDescription *currentEntity = entity;

while (!primaryKeyPropertyName && currentEntity) {
primaryKeyPropertyName = [self primaryKeyPropertyNameForEntityDescription:entity];
currentEntity = currentEntity.superentity;
}

return primaryKeyPropertyName;
}

- (NSAttributeDescription *)primaryAttributeDescription {
NSString *primaryKeyPropertyName = [self primaryKeyPropertyName];
id primaryKeyRepresentation = self.representationDictionary[primaryKeyPropertyName];
Expand All @@ -127,19 +148,6 @@ - (NSAttributeDescription *)primaryAttributeDescription {
return nil;
}

- (NSString *)primaryAttributeKeyForEntityDescription: (NSEntityDescription *)entity {
NSString *primaryKey;
NSEntityDescription *currentEntity = entity;

while (!primaryKey && currentEntity) {
NSDictionary *userInfo = [entity userInfo];
primaryKey = [userInfo valueForKey:MMRecordEntityPrimaryAttributeKey];
currentEntity = currentEntity.superentity;
}

return primaryKey;
}

- (id)primaryKeyValueFromDictionary:(NSDictionary *)dictionary {
id primaryKeyRepresentation = self.representationDictionary[self.primaryKey];

Expand Down
9 changes: 2 additions & 7 deletions Source/MMRecord/MMRecordResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,11 @@

@interface MMRecordResponse : NSObject

// Optional record primary key injection block
@property (nonatomic, copy) MMRecordOptionsEntityPrimaryKeyInjectionBlock entityPrimaryKeyInjectionBlock;

// Optional pre-population block.
@property (nonatomic, copy) MMRecordOptionsRecordPrePopulationBlock recordPrePopulationBlock;

// Designated Initializer
+ (MMRecordResponse *)responseFromResponseObjectArray:(NSArray *)responseObjectArray
initialEntity:(NSEntityDescription *)initialEntity
context:(NSManagedObjectContext *)context;
context:(NSManagedObjectContext *)context
options:(MMRecordOptions *)options;

// Records from Response Description
- (NSArray *)records;
Expand Down
18 changes: 12 additions & 6 deletions Source/MMRecord/MMRecordResponse.m
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ @interface MMRecordResponse ()
@property (nonatomic, strong) NSManagedObjectContext *context;
@property (nonatomic, strong) NSEntityDescription *initialEntity;
@property (nonatomic, copy) NSArray *responseObjectArray;
@property (nonatomic, strong) MMRecordOptions *options;
@property (nonatomic, strong) NSMutableArray *objectGraph; // Array of Protos
@property (nonatomic, strong) NSMutableDictionary *responseGroups; // Key = NSEntityDescription, Value = MMRecordResponseGroup
@end
Expand All @@ -77,11 +78,13 @@ @implementation MMRecordResponse

+ (MMRecordResponse *)responseFromResponseObjectArray:(NSArray *)responseObjectArray
initialEntity:(NSEntityDescription *)initialEntity
context:(NSManagedObjectContext *)context {
context:(NSManagedObjectContext *)context
options:(MMRecordOptions *)options {
MMRecordResponse *response = [[MMRecordResponse alloc] init];
response.context = context;
response.initialEntity = initialEntity;
response.responseObjectArray = responseObjectArray;
response.options = options;

return response;
}
Expand Down Expand Up @@ -143,7 +146,7 @@ - (MMRecordResponseGroup *)responseGroupForEntity:(NSEntityDescription *)entity
if (responseGroup == nil) {
if ([NSClassFromString([entity managedObjectClassName]) isSubclassOfClass:[MMRecord class]]) {
responseGroup = [[MMRecordResponseGroup alloc] initWithEntity:entity];
responseGroup.recordPrePopulationBlock = self.recordPrePopulationBlock;
responseGroup.recordPrePopulationBlock = self.options.recordPrePopulationBlock;
responseGroups[entityDescriptionsKey] = responseGroup;
} else {
return nil;
Expand Down Expand Up @@ -228,13 +231,16 @@ - (MMRecordProtoRecord *)protoRecordWithRecordResponseObject:(id)recordResponseO

if (proto.hasRelationshipPrimarykey == NO) {
if (proto.primaryKeyValue == nil) {
if (self.entityPrimaryKeyInjectionBlock != nil) {
proto.primaryKeyValue = self.entityPrimaryKeyInjectionBlock(proto.entity,
proto.dictionary,
parentProtoRecord);
if (self.options.entityPrimaryKeyInjectionBlock != nil) {
proto.primaryKeyValue = self.options.entityPrimaryKeyInjectionBlock(proto.entity,
proto.dictionary,
parentProtoRecord);
}
}
}
} else {
[representation.marshalerClass mergeDuplicateRecordResponseObjectDictionary:recordResponseObject
withExistingProtoRecord:proto];
}

[self uniquelyAddNewProtoRecord:proto toExistingResponseGroups:responseGroups];
Expand Down

0 comments on commit 91e1435

Please sign in to comment.