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
@@ -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.
@@ -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.
//
@@ -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.
@@ -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.
//
@@ -137,6 +137,8 @@ extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;

@end

@class MTLModel;

@interface MTLJSONAdapter (Deprecated)

+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model __attribute__((deprecated("Replaced by +JSONDictionaryFromModel:error:")));
@@ -48,7 +48,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];
@@ -63,7 +63,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]) {
@@ -92,7 +91,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);
}

@@ -156,13 +154,13 @@ - (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)mo
}
}

_model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
_model = [[self.modelClass alloc ] initWithDictionary:dictionaryValue error:error];

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

Accidental extra space here.

if (_model == nil) return nil;

return self;
}

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

self = [super init];
@@ -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
@@ -171,6 +171,6 @@ 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;

@end
@@ -68,15 +68,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 up the NSValueTransformer that should be used for any attribute that
// corresponds the given property key.
@@ -98,7 +98,7 @@ - (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key;
// 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

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

NSDictionary *managedObjectProperties = entity.propertiesByName;
MTLModel *model = [[self.modelClass alloc] init];
id<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.
@@ -150,9 +150,9 @@ - (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(
// a new object to be stored in this variable (and we don't want ARC to
// double-free or leak the old or new values).
__autoreleasing id replaceableValue = value;
if (![model validateValue:&replaceableValue forKey:key error:error]) return NO;
if (![(NSObject *)model validateValue:&replaceableValue forKey:key error:error]) return NO;

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

Can we just type the variable as NSObject * in this scope?


[model setValue:replaceableValue forKey:key];
[(NSObject *)model setValue:replaceableValue forKey:key];
return YES;
};

@@ -192,7 +192,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];
@@ -212,7 +212,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);
@@ -305,7 +305,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);
@@ -424,7 +424,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 = @{
@@ -464,7 +464,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;

@@ -541,7 +541,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:.
@@ -557,7 +557,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);
@@ -593,7 +593,7 @@ - (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key {
return nil;
}

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

NSSet *propertyKeys = [self.modelClass propertyKeysForManagedObjectUniquing];
@@ -608,7 +608,7 @@ - (NSPredicate *)uniquingPredicateForModel:(MTLModel<MTLManagedObjectSerializing

NSAssert(managedObjectKey != nil, @"%@ must map to a managed object key.", propertyKey);

id value = [model valueForKeyPath:propertyKey];
id value = [(NSObject *)model valueForKeyPath:propertyKey];

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

As above.


NSValueTransformer *transformer = [self entityAttributeTransformerForKey:propertyKey];
if ([transformer.class allowsReverseTransformation]) {
@@ -10,7 +10,7 @@
#import "EXTRuntimeExtensions.h"
#import "EXTScope.h"
#import "MTLReflection.h"
#import <objc/runtime.h>
#import "MTLModel.h"

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

This should be taken care of in the header.


// Used in archives to store the modelVersion of the archived instance.
static NSString * const MTLModelVersionKey = @"MTLModelVersion";
@@ -7,22 +7,17 @@
//

#import <Foundation/Foundation.h>
#import "MTLModel.h"

This comment has been minimized.

Copy link
@jspahrsummers

This comment has been minimized.

Copy link
@robb

robb Mar 13, 2014

Author Member

I'ma blame AppCode


// An abstract base class for model objects, using reflection to provide
// sensible default behaviors.
//
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
// the +propertyKeys method.
@interface MTLModel : NSObject <NSCopying>
@protocol MTLModel <NSObject, NSCopying>

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

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:.
+ (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;
// The default implementation combines the values corresponding to all
// +propertyKeys into a dictionary, with any nil values represented by NSNull.

This comment has been minimized.

Copy link
@robb

robb Jan 23, 2014

Author Member

This probably should be reworded so that it becomes part of the contract that nil values should map to 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.
@@ -38,31 +33,41 @@
// Returns an initialized model object, or nil if validation failed.
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

// 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.
//
// The default implementation 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;

// 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.
//
// By default, this method looks for a `-merge<Key>FromModel:` method on the

This comment has been minimized.

Copy link
@robb

robb Jan 23, 2014

Author Member

This is more of an implementation detail slash convenience of MTLModel the class and should go there accordingly.

// 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;

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

As a general rule of thumb, anything that callers can accomplish on their own—using only protocol methods—I'd prefer to leave out of the protocol, since it's just boilerplate to implement.

This comment has been minimized.

Copy link
@robb

robb Mar 13, 2014

Author Member

I'd like to have a well-defined update hook in case somebody wants to write an updating adapter.
What if we made it @optional?

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 14, 2014

Member

Isn't that -mergeValueForKey:fromModel:? I'm only proposing removing the bulk variant, since it's just the aforementioned plus +propertyKeys.

This comment has been minimized.

Copy link
@robb

robb Mar 14, 2014

Author Member

Yeah, you're right.


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

@end

// An abstract base class for model objects, using reflection to provide
// sensible default behaviors.
//
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
// the +propertyKeys method.
@interface MTLModel : NSObject <MTLModel>

// Returns a new instance of the receiver initialized using
// -initWithDictionary:error:.
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

I'd like to remove this entirely, but since this method could return a subclass for a class cluster, it seems useful to keep around. I don't like how it's separated from the protocol, though. :\

What do you think?

This comment has been minimized.

Copy link
@robb

robb Mar 13, 2014

Author Member

Hmm, class clusters could bend -initWithDictionary: to their will but that seems needlessly complicated, moving this into the protocol would mean more busywork (and the method starting with model may be confusing if the protocol gets implemented by a SKSpriteNode subclass, for example).

Any reason you'd like to get rid of it, other than reduced surface area?

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 14, 2014

Member

Tell you what, why don't we require the class method in the protocol, and put the instance method onto MTLModel?

The instance method is only really exposed for subclassing anyways. The class method is what should be called (for class clustering reasons).

This comment has been minimized.

Copy link
@robb

robb Mar 14, 2014

Author Member

👍


// Initializes the receiver with default values.
//
// This is the designated initializer for this class.
- (instancetype)init;

// Compares the receiver with another object for equality.
//
@@ -158,13 +158,13 @@ - (NSDictionary *)dictionaryValue {

#pragma mark Merging

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

SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
if (![self respondsToSelector:selector]) {
if (model != nil) {
[self setValue:[model valueForKey:key] forKey:key];
[self setValue:[(NSObject *)model valueForKey:key] forKey:key];

This comment has been minimized.

Copy link
@jspahrsummers

jspahrsummers Mar 13, 2014

Member

As above.

}

return;
@@ -178,7 +178,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];
}
@@ -176,6 +176,19 @@
expect(serializationError).to.beNil();
});

it(@"should parse model classes not inheriting from MTLModel", ^{
NSDictionary *values = @{
@"name": @"foo",
};

NSError *error = nil;
MTLConformingModel *model = [MTLJSONAdapter modelOfClass:MTLConformingModel.class fromJSONDictionary:values error:&error];
expect(model).to.beKindOf(MTLConformingModel.class);
expect(error).to.beNil();

expect(model.name).to.equal(@"foo");
});

it(@"should return an error when no suitable model class is found", ^{
NSError *error = nil;
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLSubstitutingTestModel.class fromJSONDictionary:@{} error:&error];
@@ -64,3 +64,10 @@ extern const NSInteger MTLTestModelNameMissing;
@property (nonatomic, strong) NSURL *URL;

@end

// Conforms to MTLJSONSerializing but does not inherit from the MTLModel class.
@interface MTLConformingModel : NSObject <MTLJSONSerializing>

@property (nonatomic, copy) NSString *name;

@end
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.