Break out reused functions for object mapping introspection into RKObjectUtilities

This commit is contained in:
Blake Watters
2012-09-30 12:49:59 -04:00
parent 03439f2464
commit d95fe5cd44
8 changed files with 179 additions and 72 deletions

View File

@@ -25,14 +25,12 @@
#import "NSEntityDescription+RKAdditions.h"
#import "RKLog.h"
#import "RKRelationshipMapping.h"
#import "RKObjectUtilities.h"
// Set Logging Component
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitCoreData
// Implemented in RKMappingOperation
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
@interface RKEntityMapping ()
@property (nonatomic, weak, readwrite) Class objectClass;
@property (nonatomic, strong, readwrite) NSEntityDescription *entity;

View File

@@ -10,9 +10,9 @@
#import "RKObjectMapping.h"
/**
The `RKDynamicMappingMatcher` class provides an interface for encapsulating the selection of an object mapping based on the runtime value of a property at a given key path. A matcher object is initialized with a key path, an expected value to be read from the key path, and an object mapping that is to be applied if the match evaluates to `YES`. When evaluating the match, the matcher invokes `valueForKeyPath:` on the object being matched and compares the value returned with the `expectedValue` via the `RKValueIsEqualToValue` function.
The `RKDynamicMappingMatcher` class provides an interface for encapsulating the selection of an object mapping based on the runtime value of a property at a given key path. A matcher object is initialized with a key path, an expected value to be read from the key path, and an object mapping that is to be applied if the match evaluates to `YES`. When evaluating the match, the matcher invokes `valueForKeyPath:` on the object being matched and compares the value returned with the `expectedValue` via the `RKObjectIsEqualToObject` function.
@see `RKValueIsEqualToValue()`
@see `RKObjectIsEqualToObject()`
*/
// TODO: better name? RKKeyPathMappingMatcher | RKMappingMatcher | RKKeyPathMatcher | RKMatcher | RKValueMatcher | RKPropertyMatcher
@interface RKDynamicMappingMatcher : NSObject
@@ -57,7 +57,7 @@
/**
Returns a Boolean value that indicates if the given object matches the expectations of the receiver.
The match is evaluated by invoking `valueForKeyPath:` on the give object with the value of the `keyPath` property and comparing the returned value with the `expectedValue` using the `RKValueIsEqualToValue` function.
The match is evaluated by invoking `valueForKeyPath:` on the give object with the value of the `keyPath` property and comparing the returned value with the `expectedValue` using the `RKObjectIsEqualToObject` function.
@param object The object to be evaluated.
@return `YES` if the object matches the expectations of the receiver, else `NO`.

View File

@@ -7,9 +7,7 @@
//
#import "RKDynamicMappingMatcher.h"
// Implemented in RKMappingOperation
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
#import "RKObjectUtilities.h"
///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -35,7 +33,7 @@ BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
- (BOOL)matches:(id)object
{
return RKValueIsEqualToValue([object valueForKeyPath:self.keyPath], self.expectedValue);
return RKObjectIsEqualToObject([object valueForKeyPath:self.keyPath], self.expectedValue);
}
- (NSString *)description

View File

@@ -18,7 +18,6 @@
// limitations under the License.
//
#import <objc/message.h>
#import "RKMappingOperation.h"
#import "RKMappingErrors.h"
#import "RKPropertyInspector.h"
@@ -29,41 +28,12 @@
#import "RKMappingOperationDataSource.h"
#import "RKObjectMappingOperationDataSource.h"
#import "RKDynamicMapping.h"
#import "RKObjectUtilities.h"
// Set Logging Component
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitObjectMapping
// Temporary home for object equivalancy tests
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue) {
NSCAssert(sourceValue, @"Expected sourceValue not to be nil");
NSCAssert(destinationValue, @"Expected destinationValue not to be nil");
SEL comparisonSelector;
if ([sourceValue isKindOfClass:[NSString class]] && [destinationValue isKindOfClass:[NSString class]]) {
comparisonSelector = @selector(isEqualToString:);
} else if ([sourceValue isKindOfClass:[NSNumber class]] && [destinationValue isKindOfClass:[NSNumber class]]) {
comparisonSelector = @selector(isEqualToNumber:);
} else if ([sourceValue isKindOfClass:[NSDate class]] && [destinationValue isKindOfClass:[NSDate class]]) {
comparisonSelector = @selector(isEqualToDate:);
} else if ([sourceValue isKindOfClass:[NSArray class]] && [destinationValue isKindOfClass:[NSArray class]]) {
comparisonSelector = @selector(isEqualToArray:);
} else if ([sourceValue isKindOfClass:[NSDictionary class]] && [destinationValue isKindOfClass:[NSDictionary class]]) {
comparisonSelector = @selector(isEqualToDictionary:);
} else if ([sourceValue isKindOfClass:[NSSet class]] && [destinationValue isKindOfClass:[NSSet class]]) {
comparisonSelector = @selector(isEqualToSet:);
} else {
comparisonSelector = @selector(isEqual:);
}
// Comparison magic using function pointers. See this page for details: http://www.red-sweater.com/blog/320/abusing-objective-c-with-class
// Original code courtesy of Greg Parker
// This is necessary because isEqualToNumber will return negative integer values that aren't coercable directly to BOOL's without help [sbw]
BOOL (*ComparisonSender)(id, SEL, id) = (BOOL (*)(id, SEL, id))objc_msgSend;
return ComparisonSender(sourceValue, comparisonSelector, destinationValue);
}
extern NSString * const RKObjectMappingNestingAttributeKeyName;
@interface RKMappingOperation ()
@@ -192,7 +162,7 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
- (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue
{
return RKValueIsEqualToValue(sourceValue, destinationValue);
return RKObjectIsEqualToObject(sourceValue, destinationValue);
}
- (BOOL)validateValue:(id *)value atKeyPath:(NSString *)keyPath
@@ -380,19 +350,6 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
return appliedMappings;
}
- (BOOL)isTypeACollection:(Class)type
{
Class orderedSetClass = NSClassFromString(@"NSOrderedSet");
return (type && ([type isSubclassOfClass:[NSSet class]] ||
[type isSubclassOfClass:[NSArray class]] ||
(orderedSetClass && [type isSubclassOfClass:orderedSetClass])));
}
- (BOOL)isValueACollection:(id)value
{
return [self isTypeACollection:[value class]];
}
- (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRelationshipMapping:(RKRelationshipMapping *)relationshipMapping
{
NSAssert(anObject, @"Cannot map nested object without a nested source object");
@@ -464,23 +421,23 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
}
// Handle case where incoming content is a single object, but we want a collection
Class relationshipType = [self.objectMapping classForKeyPath:relationshipMapping.destinationKeyPath];
BOOL mappingToCollection = [self isTypeACollection:relationshipType];
if (mappingToCollection && ![self isValueACollection:value]) {
Class relationshipClass = [self.objectMapping classForKeyPath:relationshipMapping.destinationKeyPath];
BOOL mappingToCollection = RKClassIsCollection(relationshipClass);
if (mappingToCollection && !RKObjectIsCollection(value)) {
Class orderedSetClass = NSClassFromString(@"NSOrderedSet");
RKLogDebug(@"Asked to map a single object into a collection relationship. Transforming to an instance of: %@", NSStringFromClass(relationshipType));
if ([relationshipType isSubclassOfClass:[NSArray class]]) {
value = [relationshipType arrayWithObject:value];
} else if ([relationshipType isSubclassOfClass:[NSSet class]]) {
value = [relationshipType setWithObject:value];
} else if (orderedSetClass && [relationshipType isSubclassOfClass:orderedSetClass]) {
value = [relationshipType orderedSetWithObject:value];
RKLogDebug(@"Asked to map a single object into a collection relationship. Transforming to an instance of: %@", NSStringFromClass(relationshipClass));
if ([relationshipClass isSubclassOfClass:[NSArray class]]) {
value = [relationshipClass arrayWithObject:value];
} else if ([relationshipClass isSubclassOfClass:[NSSet class]]) {
value = [relationshipClass setWithObject:value];
} else if (orderedSetClass && [relationshipClass isSubclassOfClass:orderedSetClass]) {
value = [relationshipClass orderedSetWithObject:value];
} else {
RKLogWarning(@"Failed to transform single object");
}
}
if ([self isValueACollection:value]) {
if (RKObjectIsCollection(value)) {
// One to many relationship
RKLogDebug(@"Mapping one to many relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath);
appliedMappings = YES;
@@ -489,7 +446,7 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
id collectionSanityCheckObject = nil;
if ([value respondsToSelector:@selector(anyObject)]) collectionSanityCheckObject = [value anyObject];
if ([value respondsToSelector:@selector(lastObject)]) collectionSanityCheckObject = [value lastObject];
if ([self isValueACollection:collectionSanityCheckObject]) {
if (RKObjectIsCollection(collectionSanityCheckObject)) {
RKLogWarning(@"WARNING: Detected a relationship mapping for a collection containing another collection. This is probably not what you want. Consider using a KVC collection operator (such as @unionOfArrays) to flatten your mappable collection.");
RKLogWarning(@"Key path '%@' yielded collection containing another collection rather than a collection of objects: %@", relationshipMapping.sourceKeyPath, value);
}
@@ -611,7 +568,7 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
{
RKLogDebug(@"Starting mapping operation...");
RKLogTrace(@"Performing mapping operation: %@", self);
// Determine the concrete mapping if we were initialized with a dynamic mapping
if ([self.mapping isKindOfClass:[RKDynamicMapping class]]) {
self.objectMapping = [(RKDynamicMapping *)self.mapping objectMappingForRepresentation:self.sourceObject];
@@ -623,7 +580,7 @@ NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters
} else if ([self.mapping isKindOfClass:[RKObjectMapping class]]) {
self.objectMapping = (RKObjectMapping *)self.mapping;
}
NSAssert(self.objectMapping, @"Cannot perform a mapping operation with an object mapping");
NSAssert(self.objectMapping, @"Cannot perform a mapping operation without an object mapping");
[self applyNestedMappings];
BOOL mappedAttributes = [self applyAttributeMappings];

View File

@@ -0,0 +1,70 @@
//
// RKObjectUtilities.h
// RestKit
//
// Created by Blake Watters on 9/30/12.
// Copyright (c) 2012 RestKit. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import <Foundation/Foundation.h>
///----------------
/// @name Functions
///----------------
/**
Returns a Boolean value that indicates whether the given objects are equal.
The actual method of comparison is dependendent upon the class of the objects given. For example, given two `NSString` objects equality would be tested using `isEqualToString:`.
@param object The first object to compare.
@param anotherObject The second object to compare.
@return `YES` if the objects are equal, otherwise `NO`.
*/
BOOL RKObjectIsEqualToObject(id object, id anotherObject);
/**
Returns a Boolean value that indicates if the given class is a collection.
The following classes are considered collections:
1. `NSSet`
1. `NSArray`
1. `NSOrderedSet`
`NSDictionary` objects are **not** considered collections as they are typically object representations.
@param aClass The class to check.
@return `YES` if the given class is a collection.
*/
BOOL RKClassIsCollection(Class aClass);
/**
Returns a Boolean value that indicates if the given object is a collection.
Implemented by invoking `RKClassIsCollection` with the class of the given object.
@param object The object to be tested.
@return `YES` if the given object is a collection, else `NO`.
@see `RKClassIsCollection`
*/
BOOL RKObjectIsCollection(id object);
/**
Returns a Boolean value that indicates if the given object is collection containing only instances of `NSManagedObject` or a class that inherits from `NSManagedObject`.
@param object The object to be tested.
@return `YES` if the object is a collection containing only `NSManagedObject` derived objects.
*/
BOOL RKObjectIsCollectionContainingOnlyManagedObjects(id object);

View File

@@ -0,0 +1,73 @@
//
// RKObjectUtilities.m
// RestKit
//
// Created by Blake Watters on 9/30/12.
// Copyright (c) 2012 RestKit. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#import <objc/message.h>
#import "RKObjectUtilities.h"
BOOL RKObjectIsEqualToObject(id object, id anotherObject) {
NSCAssert(object, @"Expected object not to be nil");
NSCAssert(anotherObject, @"Expected anotherObject not to be nil");
SEL comparisonSelector;
if ([object isKindOfClass:[NSString class]] && [anotherObject isKindOfClass:[NSString class]]) {
comparisonSelector = @selector(isEqualToString:);
} else if ([object isKindOfClass:[NSNumber class]] && [anotherObject isKindOfClass:[NSNumber class]]) {
comparisonSelector = @selector(isEqualToNumber:);
} else if ([object isKindOfClass:[NSDate class]] && [anotherObject isKindOfClass:[NSDate class]]) {
comparisonSelector = @selector(isEqualToDate:);
} else if ([object isKindOfClass:[NSArray class]] && [anotherObject isKindOfClass:[NSArray class]]) {
comparisonSelector = @selector(isEqualToArray:);
} else if ([object isKindOfClass:[NSDictionary class]] && [anotherObject isKindOfClass:[NSDictionary class]]) {
comparisonSelector = @selector(isEqualToDictionary:);
} else if ([object isKindOfClass:[NSSet class]] && [anotherObject isKindOfClass:[NSSet class]]) {
comparisonSelector = @selector(isEqualToSet:);
} else {
comparisonSelector = @selector(isEqual:);
}
// Comparison magic using function pointers. See this page for details: http://www.red-sweater.com/blog/320/abusing-objective-c-with-class
// Original code courtesy of Greg Parker
// This is necessary because isEqualToNumber will return negative integer values that aren't coercable directly to BOOL's without help [sbw]
BOOL (*ComparisonSender)(id, SEL, id) = (BOOL (*)(id, SEL, id))objc_msgSend;
return ComparisonSender(object, comparisonSelector, anotherObject);
}
BOOL RKClassIsCollection(Class aClass)
{
return (aClass && ([aClass isSubclassOfClass:[NSSet class]] ||
[aClass isSubclassOfClass:[NSArray class]] ||
[aClass isSubclassOfClass:[NSOrderedSet class]]));
}
BOOL RKObjectIsCollection(id object)
{
return RKClassIsCollection([object class]);
}
BOOL RKObjectIsCollectionContainingOnlyManagedObjects(id object)
{
if (! RKObjectIsCollection(object)) return NO;
Class managedObjectClass = NSClassFromString(@"NSManagedObject");
if (! managedObjectClass) return NO;
for (id instance in object) {
if (! [object isKindOfClass:managedObjectClass]) return NO;
}
return YES;
}

View File

@@ -23,6 +23,7 @@
#import "RKObjectMappingOperationDataSource.h"
#import "RKRelationshipMapping.h"
#import "RKErrors.h"
#import "RKObjectUtilities.h"
// Error Constants
NSString * const RKMappingTestErrorDomain = @"org.restkit.RKMappingTest.ErrorDomain";
@@ -31,8 +32,6 @@ NSString * const RKMappingTestExpectationErrorKey = @"RKMappingTestExpectationEr
NSString * const RKMappingTestValueErrorKey = @"RKMappingTestValueErrorKey";
NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVerificationFailureException";
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
///-----------------------------------------------------------------------------
///-----------------------------------------------------------------------------
@@ -217,7 +216,7 @@ BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
}
} else if (expectation.value) {
// Use RestKit comparison magic to match values
success = RKValueIsEqualToValue(event.value, expectation.value);
success = RKObjectIsEqualToObject(event.value, expectation.value);
if (! success) {
NSString *description = [NSString stringWithFormat:@"mapped to unexpected %@ value '%@'", [event.value class], event.value];

View File

@@ -456,6 +456,10 @@
259D9861155218E5008C90F5 /* RKEntityCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D985D155218E4008C90F5 /* RKEntityCache.m */; };
259D986415521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; };
259D986515521B20008C90F5 /* RKEntityCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 259D986315521B1F008C90F5 /* RKEntityCacheTest.m */; };
25A226D61618A57500952D72 /* RKObjectUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 25A226D41618A57500952D72 /* RKObjectUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
25A226D71618A57500952D72 /* RKObjectUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 25A226D41618A57500952D72 /* RKObjectUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
25A226D81618A57500952D72 /* RKObjectUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A226D51618A57500952D72 /* RKObjectUtilities.m */; };
25A226D91618A57500952D72 /* RKObjectUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 25A226D51618A57500952D72 /* RKObjectUtilities.m */; };
25A34245147D8AAA0009758D /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25A34244147D8AAA0009758D /* Security.framework */; };
25A763DB15C7240200A9DF31 /* RKSearchTokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 25A763D915C7240100A9DF31 /* RKSearchTokenizer.h */; settings = {ATTRIBUTES = (Public, ); }; };
25A763DC15C7240200A9DF31 /* RKSearchTokenizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 25A763D915C7240100A9DF31 /* RKSearchTokenizer.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -901,6 +905,8 @@
259D985C155218E4008C90F5 /* RKEntityCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKEntityCache.h; sourceTree = "<group>"; };
259D985D155218E4008C90F5 /* RKEntityCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCache.m; sourceTree = "<group>"; };
259D986315521B1F008C90F5 /* RKEntityCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKEntityCacheTest.m; sourceTree = "<group>"; };
25A226D41618A57500952D72 /* RKObjectUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKObjectUtilities.h; sourceTree = "<group>"; };
25A226D51618A57500952D72 /* RKObjectUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectUtilities.m; sourceTree = "<group>"; };
25A34244147D8AAA0009758D /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = SDKs/MacOSX10.7.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; };
25A763D915C7240100A9DF31 /* RKSearchTokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKSearchTokenizer.h; sourceTree = "<group>"; };
25A763DA15C7240100A9DF31 /* RKSearchTokenizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKSearchTokenizer.m; sourceTree = "<group>"; };
@@ -1262,6 +1268,8 @@
25AFF8F015B4CF1F0051877F /* RKMappingErrors.h */,
2598888B15EC169E006CAE95 /* RKPropertyMapping.h */,
2598888C15EC169E006CAE95 /* RKPropertyMapping.m */,
25A226D41618A57500952D72 /* RKObjectUtilities.h */,
25A226D51618A57500952D72 /* RKObjectUtilities.m */,
);
path = ObjectMapping;
sourceTree = "<group>";
@@ -1959,6 +1967,7 @@
259B96F91604CCCC0000C250 /* AFNetworking.h in Headers */,
253477F915FFBD2E002C0E4E /* NSBundle+RKAdditions.h in Headers */,
25F53F391606269400A093BE /* RKRequestOperationSubclass.h in Headers */,
25A226D61618A57500952D72 /* RKObjectUtilities.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2059,6 +2068,7 @@
259B96F81604CCCC0000C250 /* UIImageView+AFNetworking.h in Headers */,
259B96FA1604CCCC0000C250 /* AFNetworking.h in Headers */,
25F53F3A1606269400A093BE /* RKRequestOperationSubclass.h in Headers */,
25A226D71618A57500952D72 /* RKObjectUtilities.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2435,6 +2445,7 @@
259B96ED1604CCCC0000C250 /* AFURLConnectionOperation.m in Sources */,
259B96F11604CCCC0000C250 /* AFXMLRequestOperation.m in Sources */,
259B96F31604CCCC0000C250 /* UIImageView+AFNetworking.m in Sources */,
25A226D81618A57500952D72 /* RKObjectUtilities.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -2596,6 +2607,7 @@
259B96F21604CCCC0000C250 /* AFXMLRequestOperation.m in Sources */,
259B96F41604CCCC0000C250 /* UIImageView+AFNetworking.m in Sources */,
25E9C8F1161290D500647F84 /* RKObjectParameterization.m in Sources */,
25A226D91618A57500952D72 /* RKObjectUtilities.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};