Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract the MTLModel protocol #219

Merged
merged 14 commits into from Mar 14, 2014
12 changes: 7 additions & 5 deletions Mantle/MTLJSONAdapter.h
Expand Up @@ -8,10 +8,10 @@

#import <Foundation/Foundation.h>

@class MTLModel;
@protocol MTLModel;

// A MTLModel object that supports being parsed from and serialized to JSON.
@protocol MTLJSONSerializing
@protocol MTLJSONSerializing <MTLModel>
@required

// Specifies how to map property keys to different key paths in JSON.
Expand Down Expand Up @@ -66,7 +66,7 @@ extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;

// The model object that the receiver was initialized with, or that the receiver
// parsed from a JSON dictionary.
@property (nonatomic, strong, readonly) MTLModel<MTLJSONSerializing> *model;
@property (nonatomic, strong, readonly) id<MTLJSONSerializing> model;

// Attempts to parse a JSON dictionary into a model object.
//
Expand All @@ -91,7 +91,7 @@ extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;
// serializing.
//
// Returns a JSON dictionary, or nil if a serialization error occurred.
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model error:(NSError **)error;
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error;

// Initializes the receiver by attempting to parse a JSON dictionary into
// a model object.
Expand All @@ -114,7 +114,7 @@ extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;
//
// model - The model to use for JSON serialization. This argument must not be
// nil.
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model;
- (id)initWithModel:(id<MTLJSONSerializing>)model;

// Serializes the receiver's `model` into JSON.
//
Expand Down Expand Up @@ -176,6 +176,8 @@ extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;

@end

@class MTLModel;

@interface MTLJSONAdapter (Deprecated)

+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model __attribute__((deprecated("Replaced by +JSONDictionaryFromModel:error:")));
Expand Down
7 changes: 2 additions & 5 deletions Mantle/MTLJSONAdapter.m
Expand Up @@ -59,7 +59,7 @@ + (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDict
return adapter.model;
}

+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model error:(NSError **)error {
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error {
MTLJSONAdapter *adapter = [[self alloc] initWithModel:model];

return [adapter serializeToJSONDictionary:error];
Expand All @@ -74,7 +74,6 @@ - (id)init {

- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
NSParameterAssert(modelClass != nil);
NSParameterAssert([modelClass isSubclassOfClass:MTLModel.class]);
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);

if (JSONDictionary == nil || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
Expand Down Expand Up @@ -103,7 +102,6 @@ - (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)mo
return nil;
}

NSAssert([modelClass isSubclassOfClass:MTLModel.class], @"Class %@ returned from +classForParsingJSONDictionary: is not a subclass of MTLModel", modelClass);
NSAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to <MTLJSONSerializing>", modelClass);
}

Expand Down Expand Up @@ -190,7 +188,7 @@ - (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)mo
return self;
}

- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model {
- (id)initWithModel:(id<MTLJSONSerializing>)model {
NSParameterAssert(model != nil);

self = [super init];
Expand Down Expand Up @@ -272,7 +270,6 @@ - (NSString *)JSONKeyPathForPropertyKey:(NSString *)key {

- (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
NSParameterAssert(modelClass != nil);
NSParameterAssert([modelClass isSubclassOfClass:MTLModel.class]);
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);

NSMutableDictionary *result = [NSMutableDictionary dictionary];
Expand Down
6 changes: 3 additions & 3 deletions Mantle/MTLManagedObjectAdapter.h
Expand Up @@ -8,11 +8,11 @@

#import <CoreData/CoreData.h>

@class MTLModel;
@protocol MTLModel;

// A MTLModel object that supports being serialized to and from Core Data as an
// NSManagedObject.
@protocol MTLManagedObjectSerializing
@protocol MTLManagedObjectSerializing <MTLModel>
@required

// The name of the Core Data entity that the receiver serializes to and
Expand Down Expand Up @@ -171,7 +171,7 @@ extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass;
// argument must not be nil.
// error - If not NULL, this may be set to an error that occurs during
// serialization or insertion.
+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;
+ (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;

// An optional value transformer that should be used for properties of the given
// class.
Expand Down
46 changes: 34 additions & 12 deletions Mantle/MTLManagedObjectAdapter.m
Expand Up @@ -82,15 +82,15 @@ + (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)manage
// Invoked from
// +managedObjectFromModel:insertingIntoContext:processedObjects:error: after
// the receiver's properties have been initialized.
- (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
- (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;

// Performs the actual work of serialization. This method is also invoked when
// processing relationships, to create a new adapter (if needed) to handle them.
//
// `processedObjects` is a dictionary mapping MTLModels to the NSManagedObjects
// that have been created so far. It should remain alive for the full process
// of serializing the top-level MTLModel.
+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
+ (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;

// Looks at propertyKeysForManagedObjectUniquing and forms an NSPredicate
// using the uniquing keys and the provided model.
Expand All @@ -104,7 +104,7 @@ + (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
// Returns a predicate, or nil if no predicate is needed or if an error
// occurred. Clients should inspect the success parameter to decide how to
// proceed with the result.
- (NSPredicate *)uniquingPredicateForModel:(MTLModel<MTLManagedObjectSerializing> *)model success:(BOOL *)success error:(NSError **)error;
- (NSPredicate *)uniquingPredicateForModel:(id<MTLManagedObjectSerializing>)model success:(BOOL *)success error:(NSError **)error;

@end

Expand Down Expand Up @@ -146,7 +146,7 @@ - (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(
NSManagedObjectContext *context = managedObject.managedObjectContext;

NSDictionary *managedObjectProperties = entity.propertiesByName;
MTLModel *model = [[self.modelClass alloc] init];
NSObject<MTLModel> *model = [[self.modelClass alloc] init];

// Pre-emptively consider this object processed, so that we don't get into
// any cycles when processing its relationships.
Expand Down Expand Up @@ -199,7 +199,7 @@ - (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(
NSMutableArray *models = [NSMutableArray arrayWithCapacity:[relationshipCollection count]];

for (NSManagedObject *nestedObject in relationshipCollection) {
MTLModel *model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
id<MTLManagedObjectSerializing> model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
if (model == nil) return nil;

[models addObject:model];
Expand All @@ -219,7 +219,7 @@ - (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(

if (nestedObject == nil) return YES;

MTLModel *model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
id<MTLManagedObjectSerializing> model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
if (model == nil) return NO;

return setValueForKey(propertyKey, model);
Expand Down Expand Up @@ -312,7 +312,7 @@ + (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)manage
return [adapter modelFromManagedObject:managedObject processedObjects:processedObjects error:error];
}

- (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
- (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
NSParameterAssert(model != nil);
NSParameterAssert(context != nil);
NSParameterAssert(processedObjects != nil);
Expand Down Expand Up @@ -431,7 +431,7 @@ - (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
};

NSManagedObject * (^objectForRelationshipFromModel)(id) = ^ id (id model) {
if (![model isKindOfClass:MTLModel.class] || ![model conformsToProtocol:@protocol(MTLManagedObjectSerializing)]) {
if (![model conformsToProtocol:@protocol(MTLManagedObjectSerializing)]) {
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property of class %@ cannot be encoded into an NSManagedObject.", @""), [model class]];

NSDictionary *userInfo = @{
Expand Down Expand Up @@ -471,7 +471,7 @@ - (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
relationshipCollection = [NSMutableSet set];
}

for (MTLModel *model in value) {
for (id<MTLModel> model in value) {
NSManagedObject *nestedObject = objectForRelationshipFromModel(model);
if (nestedObject == nil) return NO;

Expand Down Expand Up @@ -548,7 +548,7 @@ - (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
return managedObject;
}

+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error {
+ (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error {
CFDictionaryKeyCallBacks keyCallbacks = kCFTypeDictionaryKeyCallBacks;

// Compare MTLModel keys using pointer equality, not -isEqual:.
Expand All @@ -564,7 +564,7 @@ + (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
return [self managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:error];
}

+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
+ (id)managedObjectFromModel:(id<MTLManagedObjectSerializing>)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
NSParameterAssert(model != nil);
NSParameterAssert(context != nil);
NSParameterAssert(processedObjects != nil);
Expand All @@ -578,7 +578,29 @@ + (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
return [adapter managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:error];
}

- (NSPredicate *)uniquingPredicateForModel:(MTLModel<MTLManagedObjectSerializing> *)model success:(BOOL *)success error:(NSError **)error {
- (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key {
NSParameterAssert(key != nil);

SEL selector = MTLSelectorWithKeyPattern(key, "EntityAttributeTransformer");
if ([self.modelClass respondsToSelector:selector]) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.modelClass methodSignatureForSelector:selector]];
invocation.target = self.modelClass;
invocation.selector = selector;
[invocation invoke];

__unsafe_unretained id result = nil;
[invocation getReturnValue:&result];
return result;
}

if ([self.modelClass respondsToSelector:@selector(entityAttributeTransformerForKey:)]) {
return [self.modelClass entityAttributeTransformerForKey:key];
}

return nil;
}

- (NSPredicate *)uniquingPredicateForModel:(NSObject<MTLManagedObjectSerializing> *)model success:(BOOL *)success error:(NSError **)error {
if (![self.modelClass respondsToSelector:@selector(propertyKeysForManagedObjectUniquing)]) return nil;

NSSet *propertyKeys = [self.modelClass propertyKeysForManagedObjectUniquing];
Expand Down
1 change: 0 additions & 1 deletion Mantle/MTLModel+NSCoding.m
Expand Up @@ -10,7 +10,6 @@
#import "EXTRuntimeExtensions.h"
#import "EXTScope.h"
#import "MTLReflection.h"
#import <objc/runtime.h>

// Used in archives to store the modelVersion of the archived instance.
static NSString * const MTLModelVersionKey = @"MTLModelVersion";
Expand Down
81 changes: 59 additions & 22 deletions Mantle/MTLModel.h
Expand Up @@ -21,26 +21,42 @@
// (like `NSCoding`) and equality, since it can
// be expected to stick around.
typedef enum : NSUInteger {
MTLPropertyStorageNone,
MTLPropertyStorageTransitory,
MTLPropertyStoragePermanent,
MTLPropertyStorageNone,
MTLPropertyStorageTransitory,
MTLPropertyStoragePermanent,
} MTLPropertyStorage;

// An abstract base class for model objects, using reflection to provide
// sensible default behaviors.
// This protocol defines the minimal interface that classes need to implement to
// interact with Mantle adapters.
//
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
// the +propertyKeys method.
@interface MTLModel : NSObject <NSCopying>
// It is intended for scenarios where inheriting from MTLModel is not feasible.
// However, clients are encouraged to subclass the MTLModel class if they can.
//
// Clients that wish to implement their own adapters should target classes
// conforming to this protocol rather than subclasses of MTLModel to ensure
// maximum compatibility.
@protocol MTLModel <NSObject, NSCopying>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you document the purpose of the protocol (and, specifically, the split between it and the class)?


// Returns a new instance of the receiver initialized using
// -initWithDictionary:error:.
// Initializes a new instance of the receiver using key-value coding, setting
// the keys and values in the given dictionary.
//
// dictionaryValue - Property keys and values to set on the instance. Any NSNull
// values will be converted to nil before being used. KVC
// validation methods will automatically be invoked for all of
// the properties given.
// error - If not NULL, this may be set to any error that occurs
// (like a KVC validation error).
//
// Returns an initialized model object, or nil if validation failed.
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

// Initializes the receiver with default values.
// A dictionary representing the properties of the receiver.
//
// This is the designated initializer for this class.
- (instancetype)init;
// Combines the values corresponding to all +propertyKeys into a dictionary,
// with any nil values represented by NSNull.
//
// This property must never be nil.
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;

// Initializes the receiver using key-value coding, setting the keys and values
// in the given dictionary.
Expand All @@ -56,31 +72,52 @@ typedef enum : NSUInteger {
// Returns an initialized model object, or nil if validation failed.
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

// Merges the value of the given key on the receiver with the value of the same
// key from the given model object, giving precedence to the other model object.
- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model;

// Returns the keys for all @property declarations, except for `readonly`
// properties without ivars, or properties on MTLModel itself.
+ (NSSet *)propertyKeys;

// A dictionary representing the properties of the receiver.
@end

// An abstract base class for model objects, using reflection to provide
// sensible default behaviors.
//
// The default implementation combines the values corresponding to all
// +propertyKeys into a dictionary, with any nil values represented by NSNull.
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
// the +propertyKeys method.
@interface MTLModel : NSObject <MTLModel>

// Initializes the receiver using key-value coding, setting the keys and values
// in the given dictionary.
//
// This property must never be nil.
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
// values will be converted to nil before being used. KVC
// validation methods will automatically be invoked for all of
// the properties given. If nil, this method is equivalent to
// -init.
// error - If not NULL, this may be set to any error that occurs
// (like a KVC validation error).
//
// Returns an initialized model object, or nil if validation failed.
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

// Merges the value of the given key on the receiver with the value of the same
// key from the given model object, giving precedence to the other model object.
// Initializes the receiver with default values.
//
// This is the designated initializer for this class.
- (instancetype)init;

// By default, this method looks for a `-merge<Key>FromModel:` method on the
// receiver, and invokes it if found. If not found, and `model` is not nil, the
// value for the given key is taken from `model`.
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;
- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model;

// Merges the values of the given model object into the receiver, using
// -mergeValueForKey:fromModel: for each key in +propertyKeys.
//
// `model` must be an instance of the receiver's class or a subclass thereof.
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;
- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model;

// The storage behavior of a given key.
//
Expand Down
4 changes: 2 additions & 2 deletions Mantle/MTLModel.m
Expand Up @@ -240,7 +240,7 @@ + (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey

#pragma mark Merging

- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
- (void)mergeValueForKey:(NSString *)key fromModel:(NSObject<MTLModel> *)model {
NSParameterAssert(key != nil);

SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
Expand All @@ -260,7 +260,7 @@ - (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
[invocation invoke];
}

- (void)mergeValuesForKeysFromModel:(MTLModel *)model {
- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model {
for (NSString *key in self.class.propertyKeys) {
[self mergeValueForKey:key fromModel:model];
}
Expand Down