Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Extract the MTLModel protocol #219

Merged
merged 14 commits into from

3 participants

@robb
Owner

This extracts the core interface of MTLModel into a protocol of the same name and changes the adapters to use that instead.

This gives users of the framework a greater flexibility when integrating with existing code bases where injecting MTLModel into the inheritance chain may not be an option or when MTLModel should not be exposed as a super class. (See for example #156). For existing users, it's business as usual.

Since having a class and a protocol of the same name may be confusing, I'm all for other names for the protocol or even considering to change the name of the MTLModel class to break everybody's build (See #167).

This is just a proof of concept. Some of the documentation in the protocol still refers to implementation details of the MTLModel class implementation and should be updated accordingly.

@jspahrsummers

I'm not opposed to this idea, but how would we do things like "default implementations" in this design? What happens to methods like +propertyKeys, -dictionaryValue, and our default <NSCoding> behaviors?

Mantle/MTLModel.h
@@ -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
@robb Owner
robb added a note

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

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

It would be the responsibility of the respective class to fulfil the contract of +propertyKeys and -dictionaryValue. I am not sure if the rest of Mantle actually makes use of the assumption that NSCoding is implemented, so that may or may not be optional.

In the near term, I see this mostly as a way to make Mantle more adaptable to existing inheritance chains where you'd like to use MTLJSONSerializing or mix-and-match with MTLModel subclasses. I recently subclassed SKSpriteNode and friends–which already implement NSCoding– and something like this would've been quite handy.

I think the fact that this proof of concept was such an easy search-and-replace operation goes to show that there is good encapsulation in place, codifying this could help maintaining that in the future.

Having both MTLModel the protocol and MTLModel the class seems to get hairy quite easily. What's your thought on naming the class MTLObject with regards to #167?

Mantle/MTLModel.h
((20 lines not shown))
//
-// 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.
@robb Owner
robb added a note

This probably should be reworded so that it becomes part of the contract that nil values should map to NSNull.

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

Why does the class need to be renamed to fulfill #167? Naming the class and the protocol MTLModel would make a lot of sense to me, if we can accomplish it.

@robb
Owner

It doesn't need to, but it would certainly break everybody's build :trolleybus:
Since there is already some confusion how MTLModel doesn't handle JSON by itself, I'd rather call them something different. Calling the protocol something else would work just as well for me, but I don't have an idea right now.

@jspahrsummers

It doesn't need to, but it would certainly break everybody's build

Sorry for being obtuse, but I don't fully understand the ways in which it would break builds. Don't we have the same methods implemented on MTLModel the class in the end (even if they're declared in the protocol)?

@robb
Owner

We could change the name of the class from MTLModel to e.g. MTLObject, that would break every

@interface XYModel : MTLModel
// …
@end
@jspahrsummers

Right, so my question was: if the class name stays the same—and some methods get declared in a protocol instead—in what way would builds break? Why would it affect existing code?

@robb
Owner

Ah, sorry, misread what you said.

If we stick with MTLModel for the class, everything should be fine, no matter what we call the protocol. :peach:

If we want to break everybody's build when they upgrade to 2.0 so we can make sure they follow our upgrade guide, then this may be a good opportunity to rename MTLModel the class.

@jspahrsummers

We'll probably have enough breakage no matter what. :trollface:

Seriously, though, I'm most interested in Doing Things Right™—I think that's the MTLModel name for the class and protocol. MTLObject reminds me of scary general-purpose frameworks, like Omni's Foundation.

robb added some commits
@robb robb Merge branch '2.0-development' into model-protocol
Conflicts:
	Mantle/MTLManagedObjectAdapter.m
	MantleTests/MTLTestModel.h
f5b1739
@robb robb Improve documentation 49e2bcb
@robb
Owner

Improved some of the documentation

@robb robb added the enhancement label
Mantle/MTLJSONAdapter.m
@@ -187,13 +185,13 @@ - (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)mo
}
}
- _model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
+ _model = [[self.modelClass alloc ] initWithDictionary:dictionaryValue error:error];
@jspahrsummers Owner

Accidental extra space here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLManagedObjectAdapter.m
@@ -157,9 +157,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;
@jspahrsummers Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLManagedObjectAdapter.m
@@ -593,7 +615,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];
@jspahrsummers Owner

As above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel+NSCoding.m
@@ -10,7 +10,7 @@
#import "EXTRuntimeExtensions.h"
#import "EXTScope.h"
#import "MTLReflection.h"
-#import <objc/runtime.h>
+#import "MTLModel.h"
@jspahrsummers Owner

This should be taken care of in the header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.h
@@ -7,22 +7,17 @@
//
#import <Foundation/Foundation.h>
+#import "MTLModel.h"
@jspahrsummers Owner

wat

@robb Owner
robb added a note

I'ma blame AppCode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.h
((5 lines not shown))
-// 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>
@jspahrsummers Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.h
((20 lines not shown))
//
-// This is the designated initializer for this class.
-- (instancetype)init;
+// Cmbines the values corresponding to all +propertyKeys into a dictionary,
@jspahrsummers Owner

*Combines

Extraneous space, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.h
@@ -38,31 +33,42 @@
// 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;
+
+// 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:(id<MTLModel>)model;
@jspahrsummers Owner

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.

@robb Owner
robb added a note

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?

@jspahrsummers Owner

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

@robb Owner
robb added a note

Yeah, you're right.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.h
((32 lines not shown))
-// 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.
+// Returns a new instance of the receiver initialized using
+// -initWithDictionary:error:.
++ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
@jspahrsummers Owner

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?

@robb Owner
robb added a note

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?

@jspahrsummers Owner

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).

@robb Owner
robb added a note

:+1:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mantle/MTLModel.m
((6 lines not shown))
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];
@jspahrsummers Owner

As above.

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

Let me know what you think of this protocol introduction.

@jspahrsummers

Documentation looks :sparkles:. Just the open comment threads now.

@robb
Owner

:pager:

@robb
Owner

merging

@robb robb Merge branch '2.0-development' into model-protocol
Conflicts:
	Mantle/MTLModel.h
	MantleTests/MTLTestModel.h
	MantleTests/MTLTestModel.m
3fd838e
@robb
Owner

merged

MantleTests/MTLTestModel.h
@@ -70,6 +70,15 @@
@end
+// Conforms to MTLJSONSerializing but does not inherit from the MTLModel class.
+@interface MTLConformingModel : NSObject <MTLJSONSerializing>
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
@jspahrsummers Owner

This doesn't need to be declared publicly unless it's being used in tests.

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

:iphone:

@jspahrsummers

Don't technically need this either, but it's not a big deal. :P

@jspahrsummers jspahrsummers merged commit c7ffb81 into 2.0-development
@jspahrsummers jspahrsummers deleted the model-protocol branch
@priteshshah1983

Is this going to be released soon?

@jspahrsummers

@priteshshah1983 This is currently part of the in-development 2.0 version of Mantle (occurring on the 2.0-development branch). We haven't yet created plans around releasing it, since it's still actively being worked on and changed.

@jspahrsummers jspahrsummers referenced this pull request from a commit
@jspahrsummers jspahrsummers CHANGELOG for #219 5ac1eb3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 22, 2014
  1. @robb

    Extract the MTLModel protocol

    robb authored
  2. @robb

    Add spec for conforming models

    robb authored
Commits on Feb 26, 2014
  1. @robb

    Merge branch '2.0-development' into model-protocol

    robb authored
    Conflicts:
    	Mantle/MTLManagedObjectAdapter.m
    	MantleTests/MTLTestModel.h
  2. @robb

    Improve documentation

    robb authored
Commits on Mar 13, 2014
  1. @robb

    WS

    robb authored
  2. @robb

    Change variable type

    robb authored
  3. @robb

    Remove unnecessary imports

    robb authored
  4. @robb

    Fix spelling

    robb authored
  5. @robb
Commits on Mar 14, 2014
  1. @robb
  2. @robb
  3. @robb
  4. @robb

    Merge branch '2.0-development' into model-protocol

    robb authored
    Conflicts:
    	Mantle/MTLModel.h
    	MantleTests/MTLTestModel.h
    	MantleTests/MTLTestModel.m
  5. @robb
This page is out of date. Refresh to see the latest.
View
12 Mantle/MTLJSONAdapter.h
@@ -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.
//
@@ -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:")));
View
7 Mantle/MTLJSONAdapter.m
@@ -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];
@@ -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]) {
@@ -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);
}
@@ -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];
@@ -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];
View
6 Mantle/MTLManagedObjectAdapter.h
@@ -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,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.
View
46 Mantle/MTLManagedObjectAdapter.m
@@ -82,7 +82,7 @@ + (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.
@@ -90,7 +90,7 @@ - (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model inse
// `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.
@@ -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
@@ -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.
@@ -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];
@@ -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);
@@ -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);
@@ -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 = @{
@@ -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;
@@ -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:.
@@ -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);
@@ -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];
View
1  Mantle/MTLModel+NSCoding.m
@@ -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";
View
81 Mantle/MTLModel.h
@@ -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>
-// 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.
@@ -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.
//
View
4 Mantle/MTLModel.m
@@ -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:");
@@ -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];
}
View
13 MantleTests/MTLJSONAdapterSpec.m
@@ -261,6 +261,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];
View
7 MantleTests/MTLTestModel.h
@@ -70,6 +70,13 @@ extern const NSInteger MTLTestModelNameMissing;
@end
+// Conforms to MTLJSONSerializing but does not inherit from the MTLModel class.
+@interface MTLConformingModel : NSObject <MTLJSONSerializing>
+
+@property (nonatomic, copy) NSString *name;
+
+@end
+
@interface MTLStorageBehaviorModel : MTLModel
@property (readonly, nonatomic, assign) BOOL primitive;
View
76 MantleTests/MTLTestModel.m
@@ -242,6 +242,82 @@ + (NSDictionary *)JSONKeyPathsByPropertyKey {
@end
+@interface MTLConformingModel ()
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
+
+@end
+
+@implementation MTLConformingModel
+
+#pragma mark Lifecycle
+
++ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
+ return [[self alloc] initWithDictionary:dictionaryValue error:error];
+}
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
+ self = [super init];
+ if (self == nil) return nil;
+
+ _name = dictionaryValue[@"name"];
+
+ return self;
+}
+
+#pragma mark MTLModel
+
+- (NSDictionary *)dictionaryValue {
+ if (self.name == nil) return @{};
+
+ return @{
+ @"name": self.name
+ };
+}
+
++ (NSSet *)propertyKeys {
+ return [NSSet setWithObject:@"name"];
+}
+
+- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model {
+ if ([key isEqualToString:@"name"]) {
+ self.name = [model dictionaryValue][@"name"];
+ }
+}
+
+- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model {
+ self.name = [model dictionaryValue][@"name"];
+}
+
+#pragma mark MTLJSONSerializing
+
++ (NSDictionary *)JSONKeyPathsByPropertyKey {
+ return @{
+ @"name": @"name"
+ };
+}
+
+#pragma mark NSObject
+
+- (NSUInteger)hash {
+ return self.name.hash;
+}
+
+- (BOOL)isEqual:(MTLConformingModel *)model {
+ if (self == model) return YES;
+ if (![model isMemberOfClass:self.class]) return NO;
+
+ return self.name == model.name || [self.name isEqual:model.name];
+}
+
+#pragma mark NSCopying
+
+- (id)copyWithZone:(NSZone *)zone {
+ return self;
+}
+
+@end
+
@implementation MTLStorageBehaviorModel
- (id)notIvarBacked {
Something went wrong with that request. Please try again.