Search code examples
iosrestkitrestkit-0.20

RestKit Request Value Transformer?


I am having some problems using an RKValueTransformer to serialize out an NSData image bytes to a base64 encoded string for a request. I was able to do the inverse for a response, after some help I received on stackoverflow.

Here is my code for creating the NSString to NSData value transformer, which works without issue. I found the index of the null value transformer and set it at afterNullTransformerIndex. I have also set it at index 0, but then I have to do my own null checking and this seems to work without issue.

    //add the base64 to NSData transformer after the null value transformer
RKBlockValueTransformer *base64StringToNSDataTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
    return [inputValueClass isSubclassOfClass:[NSString class]] && [outputValueClass isSubclassOfClass:[NSData class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
    RKValueTransformerTestInputValueIsKindOfClass(inputValue, [NSString class], error);
    RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputClass, [NSData class], error);

    *outputValue = [[NSData alloc] initWithBase64EncodedString:(NSString *)inputValue options:NSDataBase64DecodingIgnoreUnknownCharacters];

    return YES;
}];
base64StringToNSDataTransformer.name = @"base64StringToNSDataTransformer";
[[RKValueTransformer defaultValueTransformer] insertValueTransformer:base64StringToNSDataTransformer atIndex:afterNullTransformerIndex];

And this is my code for creating the NSData to NSString value transformer, which isn't working. I set a breakpoint in the transformationBlock: method, but it never gets invoked.:

    //add the NSData to String transformer for requests after the null value transformer
RKBlockValueTransformer *nsDataToBase64StringTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
    return [inputValueClass isSubclassOfClass:[NSData class]] && [outputValueClass isSubclassOfClass:[NSString class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
    RKValueTransformerTestInputValueIsKindOfClass(inputValue, [NSData class], error);
    RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputClass, [NSString class], error);

    *outputValue = [((NSData *)inputValue) base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength];

    return YES;
}];
nsDataToBase64StringTransformer.name = @"nsDataToBase64StringTransformer";
[[RKValueTransformer defaultValueTransformer] insertValueTransformer:nsDataToBase64StringTransformer atIndex:afterNullTransformerIndex];

Like I said, my breakpoint never gets invoked in the transformationBlock: method, but the valueTransformationWithValidationBlock: does get invoked once when serializing the request, but only when transforming from a Date to a String. Looking through the stack in the debugger and RestKit's code, I found this method in RKObjectParameterization.m:

- (void)mappingOperation:(RKMappingOperation *)operation didSetValue:(id)value forKeyPath:(NSString *)keyPath usingMapping:(RKAttributeMapping *)mapping
{
    id transformedValue = nil;
    if ([value isKindOfClass:[NSDate class]]) {
        [mapping.objectMapping.valueTransformer transformValue:value toValue:&transformedValue ofClass:[NSString class] error:nil];
    } else if ([value isKindOfClass:[NSDecimalNumber class]]) {
        // Precision numbers are serialized as strings to work around Javascript notation limits
        transformedValue = [(NSDecimalNumber *)value stringValue];
    } else if ([value isKindOfClass:[NSSet class]]) {
        // NSSets are not natively serializable, so let's just turn it into an NSArray
        transformedValue = [value allObjects];
    } else if ([value isKindOfClass:[NSOrderedSet class]]) {
        // NSOrderedSets are not natively serializable, so let's just turn it into an NSArray
        transformedValue = [value array];
    } else if (value == nil) {
        // Serialize nil values as null
        transformedValue = [NSNull null];
    } else {
        Class propertyClass = RKPropertyInspectorGetClassForPropertyAtKeyPathOfObject(mapping.sourceKeyPath, operation.sourceObject);
        if ([propertyClass isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] || [propertyClass isSubclassOfClass:NSClassFromString(@"NSCFBoolean")]) {
            transformedValue = @([value boolValue]);
        }
    }

    if (transformedValue) {
        RKLogDebug(@"Serialized %@ value at keyPath to %@ (%@)", NSStringFromClass([value class]), NSStringFromClass([transformedValue class]), value);
        [operation.destinationObject setValue:transformedValue forKeyPath:keyPath];
    }
}

It only appears that RestKit is using value transformers when value is an NSDate! Is there something that I am missing to get value transformers to work on requests?

EDIT answering Wain's questions and giving more details

This is my entity mapping code for responses. A record entity holds a collection of WTSImages:

    RKEntityMapping *imageMapping = [RKEntityMapping mappingForEntityForName:@"WTSImage" inManagedObjectStore:self.managedObjectStore];
[imageMapping addAttributeMappingsFromDictionary:@{
                                                   @"id": @"dbId",
                                                   @"status": @"status",
                                                   @"type": @"type",
                                                   @"format": @"format",
                                                   @"width": @"width",
                                                   @"height": @"height",
                                                   @"image": @"imageData"
                                                   }];
imageMapping.identificationAttributes = @[@"dbId"];
[recordMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:@"images" toKeyPath:@"images" withMapping:imageMapping]];

The WTSImage class is generated from CoreData and looks like this:

@interface WTSImage : NSManagedObject

@property (nonatomic, retain) NSNumber * dbId;
@property (nonatomic, retain) NSString * format;
@property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSData * imageData;
@property (nonatomic, retain) NSString * status;
@property (nonatomic, retain) NSString * type;
@property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) WTSCaptureDevice *captureDevice;
@property (nonatomic, retain) WTSRecord *record;
@property (nonatomic, retain) WTSTempImageSet *tempImageSet;

@end

I create a reverse record mapping and add a request descriptor.

RKEntityMapping *reverseRecordMapping = [recordMapping inverseMapping];
[self addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping:reverseRecordMapping objectClass:[WTSRecord class] rootKeyPath:@"records" method:RKRequestMethodAny]];

This is the debug log output for mapping my image object to JSON. The imageData element does not look like a normal base64 encoded string:

2014-04-10 11:02:39.537 Identify[945:60b] T restkit.object_mapping:RKMappingOperation.m:682 Mapped relationship object from keyPath 'images' to 'images'. Value: (
    {
    format = JPEG;
    height = 200;
    id = 0;
    image = <ffd8ffe0 00104a46 49460001 01000001 00010000 ffe10058 45786966 ... f77d7bf9 77b58fff d9>;
    status = C;
    type = MUGSHOT;
    width = 200;
})

And here is the POST, which my server rejects:

    2014-04-10 11:27:53.852 Identify[985:60b] T restkit.network:RKObjectRequestOperation.m:148 POST 'http://10.0.0.35:8080/Service/bs/records':
request.headers={
    Accept = "application/json";
    "Accept-Language" = "en;q=1, es;q=0.9, fr;q=0.8, de;q=0.7, ja;q=0.6, nl;q=0.5";
    "Content-Type" = "application/x-www-form-urlencoded; charset=utf-8";
    "User-Agent" = "Identify/1.0 (iPhone; iOS 7.1; Scale/2.00)";
}request.body=records[application]=Identify&records[createBy]=welcomed&records[createDt]=2014-04-10T15%3A27%3A42Z&records[description]&records[externalId]&records[groupId]=5&records[id]=0&records[images][][format]=JPEG&records[images][][height]=200&records[images][][id]=0&records[images][][image]=%3Cffd8ffe0%2000104a46%2049460001%2001000001%20000.......d773%20ffd9%3E&records[images][][status]=C&records[images][][type]=MUGSHOT&records[images][][width]=200&records[locked]&records[modifyBy]&records[modifyDt]&records[priv]

Solution

  • In RKObjectMapping classForKeyPath:, it is unable to find the class for my 'image' property. It appears that the _objectClass is a NSMutableDictionary rather than a WTSImage. This is causing the method to return a nil propertyClass

    That makes sense, because the mapping destination for a request is NSMutableDictionary (and the source object is WTSImage. So, it doesn't apply any specific transformations and falls through to mappingOperation:didSetValue:forKeyPath:usingMapping: which you have already seen doesn't cater for this situation.

    I think this will be hard to deal with using a transformer.

    The only way I can think to deal with it right now is to add a method to WTSImage, say base64Image which returns the transformed image data and use that in your mapping (which means you won't be able to use [recordMapping inverseMapping]).