mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-01-12 22:51:50 +08:00
481 lines
23 KiB
Objective-C
481 lines
23 KiB
Objective-C
//
|
|
// RKMappingTest.m
|
|
// RestKit
|
|
//
|
|
// Created by Blake Watters on 2/17/12.
|
|
// Copyright (c) 2009-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 "RKMappingTest.h"
|
|
#import "RKEntityMapping.h"
|
|
#import "RKObjectMappingOperationDataSource.h"
|
|
#import "RKRelationshipMapping.h"
|
|
#import "RKErrors.h"
|
|
#import "RKObjectUtilities.h"
|
|
#import "RKLog.h"
|
|
|
|
// Core Data
|
|
#import "RKConnectionDescription.h"
|
|
#import "RKConnectionTestExpectation.h"
|
|
#import "RKFetchRequestManagedObjectCache.h"
|
|
#import "RKManagedObjectMappingOperationDataSource.h"
|
|
|
|
// Error Constants
|
|
NSString * const RKMappingTestErrorDomain = @"org.restkit.RKMappingTest.ErrorDomain";
|
|
NSString * const RKMappingTestEventErrorKey = @"RKMappingTestEventErrorKey";
|
|
NSString * const RKMappingTestExpectationErrorKey = @"RKMappingTestExpectationErrorKey";
|
|
NSString * const RKMappingTestValueErrorKey = @"RKMappingTestValueErrorKey";
|
|
NSString * const RKMappingTestVerificationFailureException = @"RKMappingTestVerificationFailureException";
|
|
|
|
///-----------------------------------------------------------------------------
|
|
///-----------------------------------------------------------------------------
|
|
|
|
@interface RKMappingTestEvent : NSObject
|
|
|
|
@property (nonatomic, strong, readonly) RKPropertyMapping *propertyMapping;
|
|
@property (nonatomic, strong, readonly) RKConnectionDescription *connection;
|
|
@property (nonatomic, strong, readonly) id value;
|
|
|
|
@property (weak, nonatomic, readonly) NSString *sourceKeyPath;
|
|
@property (weak, nonatomic, readonly) NSString *destinationKeyPath;
|
|
|
|
+ (RKMappingTestEvent *)eventWithMapping:(RKPropertyMapping *)propertyMapping value:(id)value;
|
|
+ (RKMappingTestEvent *)eventWithConnection:(RKConnectionDescription *)connection value:(id)value;
|
|
|
|
@end
|
|
|
|
@interface RKMappingTestEvent ()
|
|
@property (nonatomic, strong, readwrite) id value;
|
|
@property (nonatomic, strong, readwrite) RKPropertyMapping *propertyMapping;
|
|
@property (nonatomic, strong, readwrite) RKConnectionDescription *connection;
|
|
@end
|
|
|
|
@implementation RKMappingTestEvent
|
|
|
|
+ (RKMappingTestEvent *)eventWithMapping:(RKPropertyMapping *)propertyMapping value:(id)value
|
|
{
|
|
RKMappingTestEvent *event = [RKMappingTestEvent new];
|
|
event.value = value;
|
|
event.propertyMapping = propertyMapping;
|
|
|
|
return event;
|
|
}
|
|
|
|
+ (RKMappingTestEvent *)eventWithConnection:(RKConnectionDescription *)connection value:(id)value
|
|
{
|
|
RKMappingTestEvent *event = [RKMappingTestEvent new];
|
|
event.connection = connection;
|
|
event.value = value;
|
|
return event;
|
|
}
|
|
|
|
- (NSString *)sourceKeyPath
|
|
{
|
|
return [self.propertyMapping sourceKeyPath];
|
|
}
|
|
|
|
- (NSString *)destinationKeyPath
|
|
{
|
|
return [self.propertyMapping destinationKeyPath];
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
if (self.propertyMapping) {
|
|
return [NSString stringWithFormat:@"%@ mapped sourceKeyPath '%@' => destinationKeyPath '%@' with value: %@>", [self class],
|
|
self.sourceKeyPath, self.destinationKeyPath, self.value];
|
|
} else if (self.connection) {
|
|
if ([self.connection isForeignKeyConnection]) {
|
|
return [NSString stringWithFormat:@"%@ connected Relationship '%@' using attributes '%@' to value: %@>", [self class],
|
|
[self.connection.relationship name], [self.connection.attributes valueForKey:@"name"], self.value];
|
|
} else if ([self.connection isKeyPathConnection]) {
|
|
return [NSString stringWithFormat:@"%@ connected Relationship '%@' using keyPath '%@' to value: %@>", [self class],
|
|
[self.connection.relationship name], self.connection.keyPath, self.value];
|
|
}
|
|
}
|
|
|
|
return [super description];
|
|
}
|
|
|
|
@end
|
|
|
|
///-----------------------------------------------------------------------------
|
|
///-----------------------------------------------------------------------------
|
|
|
|
@interface RKMappingTest () <RKMappingOperationDelegate>
|
|
@property (nonatomic, strong, readwrite) RKMapping *mapping;
|
|
@property (nonatomic, strong, readwrite) id sourceObject;
|
|
@property (nonatomic, strong, readwrite) id destinationObject;
|
|
@property (nonatomic, strong) NSMutableArray *expectations;
|
|
@property (nonatomic, strong) NSMutableArray *events;
|
|
@property (nonatomic, assign, getter = hasPerformedMapping) BOOL performedMapping;
|
|
|
|
// Method Definitions for old compilers
|
|
- (void)performMapping;
|
|
- (void)verifyExpectation:(RKPropertyMappingTestExpectation *)expectation;
|
|
|
|
@end
|
|
|
|
@implementation RKMappingTest
|
|
|
|
+ (instancetype)testForMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject
|
|
{
|
|
return [[self alloc] initWithMapping:mapping sourceObject:sourceObject destinationObject:destinationObject];
|
|
}
|
|
|
|
- (id)initWithMapping:(RKMapping *)mapping sourceObject:(id)sourceObject destinationObject:(id)destinationObject
|
|
{
|
|
NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object");
|
|
NSAssert(mapping != nil, @"Cannot perform a mapping operation without a mapping");
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
self.sourceObject = sourceObject;
|
|
self.destinationObject = destinationObject;
|
|
self.mapping = mapping;
|
|
self.expectations = [NSMutableArray new];
|
|
self.events = [NSMutableArray new];
|
|
self.performedMapping = NO;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)addExpectation:(id)expectation
|
|
{
|
|
NSParameterAssert(expectation);
|
|
if (![expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] && ![expectation isKindOfClass:[RKConnectionTestExpectation class]]) {
|
|
[NSException raise:NSInvalidArgumentException
|
|
format:@"Invalid expectation: expected an object of type `%@` or `%@`, but instead got a `%@`",
|
|
[RKPropertyMappingTestExpectation class], [RKConnectionTestExpectation class], expectation];
|
|
}
|
|
[self.expectations addObject:expectation];
|
|
}
|
|
|
|
- (RKMappingTestEvent *)eventMatchingExpectation:(id)expectation
|
|
{
|
|
for (RKMappingTestEvent *event in [self.events copy]) {
|
|
if ([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]]) {
|
|
RKPropertyMappingTestExpectation *propertyExpectation = (RKPropertyMappingTestExpectation *) expectation;
|
|
if ([event.sourceKeyPath isEqualToString:propertyExpectation.sourceKeyPath] && [event.destinationKeyPath isEqualToString:propertyExpectation.destinationKeyPath]) {
|
|
return event;
|
|
}
|
|
} else if ([expectation isKindOfClass:[RKConnectionTestExpectation class]]) {
|
|
RKConnectionTestExpectation *connectionExpectation = (RKConnectionTestExpectation *) expectation;
|
|
if ([[event.connection.relationship name] isEqualToString:connectionExpectation.relationshipName]) {
|
|
return event;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSError *)errorForExpectation:(RKPropertyMappingTestExpectation *)expectation
|
|
withCode:(NSInteger)errorCode
|
|
userInfo:(NSDictionary *)userInfo
|
|
description:(NSString *)description
|
|
reason:(NSString *)reason
|
|
{
|
|
NSMutableDictionary *fullUserInfo = [userInfo mutableCopy];
|
|
[fullUserInfo setObject:description forKey:NSLocalizedDescriptionKey];
|
|
[fullUserInfo setObject:reason forKey:NSLocalizedFailureReasonErrorKey];
|
|
return [NSError errorWithDomain:RKMappingTestErrorDomain code:errorCode userInfo:fullUserInfo];
|
|
}
|
|
|
|
- (BOOL)event:(RKMappingTestEvent *)event satisfiesExpectation:(id)expectation error:(NSError **)error
|
|
{
|
|
BOOL success = NO;
|
|
|
|
NSDictionary *userInfo = @{ RKMappingTestEventErrorKey : event,
|
|
RKMappingTestExpectationErrorKey : expectation };
|
|
if ([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]]) {
|
|
RKPropertyMappingTestExpectation *propertyExpectation = (RKPropertyMappingTestExpectation *)expectation;
|
|
if (propertyExpectation.evaluationBlock) {
|
|
// Let the expectation block evaluate the match
|
|
NSError *blockError = nil;
|
|
success = propertyExpectation.evaluationBlock(expectation, event.propertyMapping, event.value, &blockError);
|
|
|
|
if (! success) {
|
|
if (blockError) {
|
|
// If the block has given us an error, use the reason
|
|
NSMutableDictionary *mutableUserInfo = [userInfo mutableCopy];
|
|
[mutableUserInfo setValue:blockError forKey:NSUnderlyingErrorKey];
|
|
NSString *reason = [NSString stringWithFormat:@"expected to %@ with value %@ '%@', but it did not",
|
|
expectation, [event.value class], event.value];
|
|
*error = [self errorForExpectation:expectation
|
|
withCode:RKMappingTestEvaluationBlockError
|
|
userInfo:mutableUserInfo
|
|
description:[blockError localizedDescription]
|
|
reason:reason];
|
|
|
|
*error = blockError;
|
|
} else {
|
|
NSString *description = [NSString stringWithFormat:@"evaluation block returned `NO` for %@ value '%@'", [event.value class], event.value];
|
|
NSString *reason = [NSString stringWithFormat:@"expected to %@ with value %@ '%@', but it did not",
|
|
expectation, [event.value class], event.value];
|
|
*error = [self errorForExpectation:expectation
|
|
withCode:RKMappingTestEvaluationBlockError
|
|
userInfo:userInfo
|
|
description:description
|
|
reason:reason];
|
|
}
|
|
}
|
|
} else if (propertyExpectation.value) {
|
|
// Use RestKit comparison magic to match values
|
|
success = RKObjectIsEqualToObject(event.value, propertyExpectation.value);
|
|
|
|
if (! success) {
|
|
NSString *description = [NSString stringWithFormat:@"mapped to unexpected %@ value '%@'", [event.value class], event.value];
|
|
NSString *reason = [NSString stringWithFormat:@"expected to %@, but instead got %@ '%@'",
|
|
expectation, [event.value class], event.value];
|
|
if (error) *error = [self errorForExpectation:expectation
|
|
withCode:RKMappingTestValueInequalityError
|
|
userInfo:userInfo
|
|
description:description
|
|
reason:reason];
|
|
}
|
|
} else if (propertyExpectation.mapping) {
|
|
if ([event.propertyMapping isKindOfClass:[RKRelationshipMapping class]]) {
|
|
// Check the mapping that was used to map the relationship
|
|
RKMapping *relationshipMapping = [(RKRelationshipMapping *)event.propertyMapping mapping];
|
|
success = [relationshipMapping isEqualToMapping:propertyExpectation.mapping];
|
|
|
|
if (! success) {
|
|
NSString *description = [NSString stringWithFormat:@"mapped using unexpected mapping: %@", relationshipMapping];
|
|
NSString *reason = [NSString stringWithFormat:@"expected to %@, but was instead mapped using: %@",
|
|
expectation, relationshipMapping];
|
|
if (error) *error = [self errorForExpectation:expectation
|
|
withCode:RKMappingTestMappingMismatchError
|
|
userInfo:userInfo
|
|
description:description
|
|
reason:reason];
|
|
}
|
|
} else {
|
|
NSString *description = [NSString stringWithFormat:@"expected a property mapping of type `RKRelationshipMapping` but instead got a `%@`", [propertyExpectation.mapping class]];
|
|
NSString *reason = [NSString stringWithFormat:@"expected to %@, but instead of a `RKRelationshipMapping` got a `%@`",
|
|
expectation, [propertyExpectation.mapping class]];
|
|
if (error) *error = [self errorForExpectation:expectation
|
|
withCode:RKMappingTestMappingMismatchError
|
|
userInfo:userInfo
|
|
description:description
|
|
reason:reason];
|
|
|
|
// Error message here that a relationship was not mapped!!!
|
|
return NO;
|
|
}
|
|
} else {
|
|
// We only wanted to know that a mapping occured between the keyPaths
|
|
success = YES;
|
|
}
|
|
} else if ([expectation isKindOfClass:[RKConnectionTestExpectation class]]) {
|
|
RKConnectionTestExpectation *connectionExpectation = (RKConnectionTestExpectation *)expectation;
|
|
id expectedValue = connectionExpectation.value;
|
|
id connectedValue = event.value;
|
|
|
|
// Check that the connection attributes match
|
|
if (connectionExpectation.attributes) {
|
|
RKMappingTestCondition([connectionExpectation.attributes isEqualToDictionary:event.connection.attributes], RKMappingTestValueInequalityError, error, @"established connection using unexpected attributes: %@", event.connection.attributes);
|
|
}
|
|
|
|
// Wrong objects
|
|
if (expectedValue) {
|
|
RKMappingTestCondition(connectedValue, RKMappingTestValueInequalityError, error, @"unexpectedly connected to nil object set (%@)", connectedValue);
|
|
|
|
if ([connectedValue isKindOfClass:[NSManagedObject class]] && [connectionExpectation.value isKindOfClass:[NSManagedObject class]]) {
|
|
// Do a managed object ID comparison
|
|
RKMappingTestCondition([[connectedValue objectID] isEqual:[expectedValue objectID]], RKMappingTestValueInequalityError, error, @"connected to unexpected managed object: %@", connectedValue);
|
|
} else {
|
|
// If we are connecting to a collection of managed objects, do a comparison of object IDs
|
|
if (RKObjectIsCollectionContainingOnlyManagedObjects(connectedValue) && RKObjectIsCollectionContainingOnlyManagedObjects(expectedValue)) {
|
|
RKMappingTestCondition(RKObjectIsEqualToObject([connectedValue valueForKeyPath:@"objectID"], [expectedValue valueForKeyPath:@"objectID"]), RKMappingTestValueInequalityError, error, @"connected to unexpected %@ value '%@'", [connectedValue class], connectedValue);
|
|
} else {
|
|
RKMappingTestCondition(RKObjectIsEqualToObject(connectedValue, expectedValue), RKMappingTestValueInequalityError, error, @"connected to unexpected %@ value '%@'", [connectedValue class], connectedValue);
|
|
}
|
|
}
|
|
} else {
|
|
RKMappingTestCondition(connectedValue == nil, RKMappingTestValueInequalityError, error, @"unexpectedly connected to non-nil object set (%@)", connectedValue);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
- (id<RKMappingOperationDataSource>)dataSourceForMappingOperation:(RKMappingOperation *)mappingOperation
|
|
{
|
|
// If we have been given an explicit data source, use it
|
|
if (self.mappingOperationDataSource) return self.mappingOperationDataSource;
|
|
|
|
#ifdef _COREDATADEFINES_H
|
|
if ([self.mapping isKindOfClass:[RKEntityMapping class]]) {
|
|
NSAssert(self.managedObjectContext, @"Cannot test an `RKEntityMapping` with a nil managed object context.");
|
|
id<RKManagedObjectCaching> managedObjectCache = self.managedObjectCache ?: [RKFetchRequestManagedObjectCache new];
|
|
RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:self.managedObjectContext cache:managedObjectCache];
|
|
|
|
// Configure an operation queue to enable easy testing of connection operations
|
|
NSOperationQueue *operationQueue = [NSOperationQueue new];
|
|
dataSource.operationQueue = operationQueue;
|
|
dataSource.parentOperation = mappingOperation;
|
|
return dataSource;
|
|
} else {
|
|
return [RKObjectMappingOperationDataSource new];
|
|
}
|
|
#else
|
|
return [RKObjectMappingOperationDataSource new];
|
|
#endif
|
|
}
|
|
|
|
- (void)performMapping
|
|
{
|
|
// Ensure repeated invocations of verify only result in a single mapping operation
|
|
if (! self.hasPerformedMapping) {
|
|
id sourceObject = self.rootKeyPath ? [self.sourceObject valueForKeyPath:self.rootKeyPath] : self.sourceObject;
|
|
RKMappingOperation *mappingOperation = [[RKMappingOperation alloc] initWithSourceObject:sourceObject destinationObject:self.destinationObject mapping:self.mapping];
|
|
mappingOperation.dataSource = [self dataSourceForMappingOperation:mappingOperation];
|
|
NSError *error = nil;
|
|
mappingOperation.delegate = self;
|
|
[mappingOperation start];
|
|
if (mappingOperation.error) {
|
|
[NSException raise:NSInternalInconsistencyException format:@"%p: failed with error: %@\n%@ during mapping from %@ to %@ with mapping %@",
|
|
self, error, [self description], self.sourceObject, self.destinationObject, self.mapping];
|
|
}
|
|
|
|
// Let the connection operations execute to completion
|
|
Class managedObjectMappingOperationDataSourceClass = NSClassFromString(@"RKManagedObjectMappingOperationDataSource");
|
|
if ([mappingOperation.dataSource isKindOfClass:managedObjectMappingOperationDataSourceClass]) {
|
|
NSOperationQueue *operationQueue = [(RKManagedObjectMappingOperationDataSource *)mappingOperation.dataSource operationQueue];
|
|
if (! [operationQueue isEqual:[NSOperationQueue mainQueue]]) {
|
|
[operationQueue waitUntilAllOperationsAreFinished];
|
|
}
|
|
}
|
|
|
|
self.performedMapping = YES;
|
|
|
|
// Get the destination object from the mapping operation
|
|
if (! self.destinationObject) self.destinationObject = mappingOperation.destinationObject;
|
|
}
|
|
}
|
|
|
|
- (void)verifyExpectation:(RKPropertyMappingTestExpectation *)expectation
|
|
{
|
|
RKMappingTestEvent *event = [self eventMatchingExpectation:expectation];
|
|
if (event) {
|
|
// Found a matching event, check if it satisfies the expectation
|
|
NSError *error = nil;
|
|
if (! [self event:event satisfiesExpectation:expectation error:&error]) {
|
|
NSDictionary *userInfo = @{ NSUnderlyingErrorKey: error,
|
|
RKMappingTestEventErrorKey: event,
|
|
RKMappingTestExpectationErrorKey: expectation };
|
|
[[NSException exceptionWithName:RKMappingTestVerificationFailureException
|
|
reason:[error localizedDescription]
|
|
userInfo:userInfo] raise];
|
|
}
|
|
} else {
|
|
// No match
|
|
[NSException raise:NSInternalInconsistencyException format:@"%@: expectation not satisfied: %@, but did not.",
|
|
[self description], [expectation summary]];
|
|
}
|
|
}
|
|
|
|
- (void)verify
|
|
{
|
|
[self performMapping];
|
|
|
|
for (RKPropertyMappingTestExpectation *expectation in self.expectations) {
|
|
[self verifyExpectation:expectation];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Evaluating Expectations
|
|
|
|
- (BOOL)evaluate
|
|
{
|
|
[self performMapping];
|
|
|
|
for (RKPropertyMappingTestExpectation *expectation in self.expectations) {
|
|
if (! [self evaluateExpectation:expectation error:nil]) return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)evaluateExpectation:(id)expectation error:(NSError **)error
|
|
{
|
|
NSParameterAssert(expectation);
|
|
Class connectionTestExpectation = NSClassFromString(@"RKConnectionTestExpectation");
|
|
NSAssert([expectation isKindOfClass:[RKPropertyMappingTestExpectation class]] || (connectionTestExpectation && [expectation isKindOfClass:connectionTestExpectation]), @"Must be an instance of `RKPropertyMappingTestExpectation` or `RKConnectionTestExpectation`");
|
|
[self performMapping];
|
|
|
|
RKMappingTestEvent *event = [self eventMatchingExpectation:expectation];
|
|
if (event) {
|
|
if (! [self event:event satisfiesExpectation:expectation error:error]) {
|
|
return NO;
|
|
}
|
|
} else {
|
|
if (error) {
|
|
NSDictionary *userInfo = @{
|
|
RKMappingTestExpectationErrorKey : expectation,
|
|
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"expected to %@, but did not.", [expectation summary]],
|
|
NSLocalizedFailureReasonErrorKey : [NSString stringWithFormat:@"%@: %@, but did not.", [self description], [expectation summary]]
|
|
};
|
|
*error = [NSError errorWithDomain:RKMappingTestErrorDomain code:RKMappingTestUnsatisfiedExpectationError userInfo:userInfo];
|
|
};
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSString *)expectationsDescription
|
|
{
|
|
return [self.expectations valueForKey:@"description"];
|
|
}
|
|
|
|
- (NSString *)eventsDescription
|
|
{
|
|
return [self.events valueForKey:@"description"];
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"%@ Expectations: %@\nEvents: %@",
|
|
[self class], [self expectationsDescription], [self eventsDescription]];
|
|
}
|
|
|
|
#pragma mark - RKMappingOperationDelegate
|
|
|
|
- (void)addEvent:(RKMappingTestEvent *)event
|
|
{
|
|
@synchronized(self.events) { [self.events addObject:event]; };
|
|
}
|
|
|
|
- (void)mappingOperation:(RKMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping
|
|
{
|
|
[self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]];
|
|
}
|
|
|
|
- (void)mappingOperation:(RKMappingOperation *)operation didNotSetUnchangedValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping
|
|
{
|
|
[self addEvent:[RKMappingTestEvent eventWithMapping:mapping value:value]];
|
|
}
|
|
|
|
- (void)mappingOperation:(RKMappingOperation *)operation didConnectRelationship:(NSRelationshipDescription *)relationship toValue:(id)value usingConnection:(RKConnectionDescription *)connection
|
|
{
|
|
[self addEvent:[RKMappingTestEvent eventWithConnection:connection value:value]];
|
|
}
|
|
|
|
@end
|