Skip to content

Commit

Permalink
Break out reused functions for object mapping introspection into RKOb…
Browse files Browse the repository at this point in the history
…jectUtilities
  • Loading branch information
blakewatters committed Sep 30, 2012
1 parent 03439f2 commit d95fe5c
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 72 deletions.
4 changes: 1 addition & 3 deletions Code/CoreData/RKEntityMapping.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions Code/ObjectMapping/RKDynamicMappingMatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down
6 changes: 2 additions & 4 deletions Code/ObjectMapping/RKDynamicMappingMatcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
//

#import "RKDynamicMappingMatcher.h"

// Implemented in RKMappingOperation
BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);
#import "RKObjectUtilities.h"

///////////////////////////////////////////////////////////////////////////////////////////////////

Expand All @@ -35,7 +33,7 @@ - (id)initWithKeyPath:(NSString *)keyPath expectedValue:(id)expectedValue object

- (BOOL)matches:(id)object
{
return RKValueIsEqualToValue([object valueForKeyPath:self.keyPath], self.expectedValue);
return RKObjectIsEqualToObject([object valueForKeyPath:self.keyPath], self.expectedValue);
}

- (NSString *)description
Expand Down
75 changes: 16 additions & 59 deletions Code/ObjectMapping/RKMappingOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
// limitations under the License.
//

#import <objc/message.h>
#import "RKMappingOperation.h"
#import "RKMappingErrors.h"
#import "RKPropertyInspector.h"
Expand All @@ -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 ()
Expand Down Expand Up @@ -192,7 +162,7 @@ - (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)desti

- (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue
{
return RKValueIsEqualToValue(sourceValue, destinationValue);
return RKObjectIsEqualToObject(sourceValue, destinationValue);
}

- (BOOL)validateValue:(id *)value atKeyPath:(NSString *)keyPath
Expand Down Expand Up @@ -380,19 +350,6 @@ - (BOOL)applyAttributeMappings
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");
Expand Down Expand Up @@ -464,23 +421,23 @@ - (BOOL)applyRelationshipMappings
}

// 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;
Expand All @@ -489,7 +446,7 @@ - (BOOL)applyRelationshipMappings
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);
}
Expand Down Expand Up @@ -611,7 +568,7 @@ - (void)start
{
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];
Expand All @@ -623,7 +580,7 @@ - (void)start
} 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];
Expand Down
70 changes: 70 additions & 0 deletions Code/ObjectMapping/RKObjectUtilities.h
Original file line number Diff line number Diff line change
@@ -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);
73 changes: 73 additions & 0 deletions Code/ObjectMapping/RKObjectUtilities.m
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 2 additions & 3 deletions Code/Testing/RKMappingTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -31,8 +32,6 @@
NSString * const RKMappingTestValueErrorKey = @"RKMappingTestValueErrorKey";
NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVerificationFailureException";

BOOL RKValueIsEqualToValue(id sourceValue, id destinationValue);

///-----------------------------------------------------------------------------
///-----------------------------------------------------------------------------

Expand Down Expand Up @@ -217,7 +216,7 @@ - (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(RKMappingTestExp
}
} 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];
Expand Down
Loading

0 comments on commit d95fe5c

Please sign in to comment.