Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value transformers are not applied on POST body data #1949

Closed
emlynmac opened this issue Jun 11, 2014 · 10 comments · May be fixed by #2475
Closed

Value transformers are not applied on POST body data #1949

emlynmac opened this issue Jun 11, 2014 · 10 comments · May be fixed by #2475
Labels

Comments

@emlynmac
Copy link

I use NSManagedObjects to replicate a remote endpoint. One of these parameters is passed from the server as BASE64 encoded binary data, which I have implemented a custom NSObject RKValueTransforming class to handle. The API also uses a UNIX time convention, which works well when retrieving the object.

When POSTing back to the server, these values are not transformed, and in some cases will cause a crash in the JSON serialization layer - when it does not adhere to NSCoding.

After digging a little through the code, I see that RKMappingOperations handle the work of transforming the parameters in the - (BOOL)transformValue:(id)inputValue toValue:(__autoreleasing id *)outputValue withPropertyMapping:(RKPropertyMapping *)propertyMapping error: method.

I'm going to see how hard it would be to add this to the outgoing POST requests so that values are transformed on the way out too.

@segiddins
Copy link
Member

Create a custom RKValueTransformer for this attribute.

@emlynmac
Copy link
Author

I guess the markdown destroyed the part where I said I had a custom RKValueTransformer.

The issue is that the RKValueTransformers are NOT used prior to the JSON serialization layer on the way out. Please re-open the issue, or provide me with a code path to follow 'cas I can't see it.

@segiddins segiddins reopened this Jun 11, 2014
@segiddins
Copy link
Member

Please share the code in which you assign the transformer to the mapping?

@emlynmac
Copy link
Author

The transformer is global:

 // Initialize RestKit
 RKObjectManager *objectManager = [[RKObjectManager alloc] initWithHTTPClient:client];
    objectManager.requestSerializationMIMEType = RKMIMETypeJSON;

    // Add Base64 image transformer
    RKCompoundValueTransformer *valueTransformer = [RKValueTransformer defaultValueTransformer];

    [valueTransformer removeValueTransformer:[RKValueTransformer iso8601TimestampToDateValueTransformer]];
    [valueTransformer addValueTransformer:[MyBase64ImageTransformer new]];

@emlynmac
Copy link
Author

Following the code path for creating a POST request, it looks like the issue is in RKObjectParameterization.m

- (BOOL)transformValue:(id)inputValue toValue:(__autoreleasing id *)outputValue withPropertyMapping:(RKPropertyMapping *)propertyMapping error:(NSError *__autoreleasing *)error
{
    if (! inputValue) {
        *outputValue = nil;
        // We only want to consider the transformation successful and assign nil if the mapping calls for it
        return propertyMapping.objectMapping.assignsDefaultValueForMissingAttributes;
    }
    Class transformedValueClass = propertyMapping.propertyValueClass ?: [self.objectMapping classForKeyPath:propertyMapping.destinationKeyPath];
    if (! transformedValueClass) {
        *outputValue = inputValue;
        return YES;
    }
    RKLogTrace(@"Found transformable value at keyPath '%@'. Transforming from class '%@' to '%@'", propertyMapping.sourceKeyPath, NSStringFromClass([inputValue class]), NSStringFromClass(transformedValueClass));
    BOOL success = [propertyMapping.valueTransformer transformValue:inputValue toValue:outputValue ofClass:transformedValueClass error:error];
    if (! success) RKLogError(@"Failed transformation of value at keyPath '%@' to representation of type '%@': %@", propertyMapping.sourceKeyPath, transformedValueClass, *error);
    return success;
}

Issue is in

Class transformedValueClass = propertyMapping.propertyValueClass ?: [self.objectMapping classForKeyPath:propertyMapping.destinationKeyPath];

This consistently returns nil for the value class, so no transform is done. If this returned a NSString class type, then the value transformer should get invoked - if I understand this correctly...

@segiddins
Copy link
Member

@blakewatters this should probably change to use a value transformer for everything so it is customizable...

@emlynmac
Copy link
Author

I figured out what is up. From the documentation on RKPropertyMapping:

/**
 Specifies the class used to represent the value of the mapped property. A value of `Nil` (which is the default value) indicates the property class is to be determined by runtime introspection.

 In cases where run-time type introspection cannot be performed (such as during object parameterization) you can specify the class used to represent the value of the property being mapped.
 */
@property (nonatomic, strong) Class propertyValueClass;

I was able to set this for value I have, which invokes the value transformer.

@SuperTango
Copy link

@emlynmac: Can you post a Gist or something of your final code? I'm trying to do the same thing with a NSURL, and there are a couple of missing pieces that I'm not following.

@rplankenhorn
Copy link

@emlynmac Can you elaborate on your fix? I am having a similar issue and can't get my custom transformer to be used at the serialization layer.

@gastonmorixe
Copy link

In my case I needed to convert for responses and requests millisecond timestamps. Responses worked by default after set a custom value transformer.

The problem was that it wasn't taken into consideration at request time.

In request mapping I solved it by setting the propertyValueClass as specified above.

[mapping addAttributeMappingsFromDictionary:@{ @"content"               : @"content",
                                               @"messageLocalID"        : @"local_id",
                                               @"createdLocallyAt"      : @"created_locally_at",
                                               }];

[mapping.propertyMappings enumerateObjectsUsingBlock:^(RKAttributeMapping*  _Nonnull attributeMapping, NSUInteger idx, BOOL * _Nonnull stop) {
    if ([attributeMapping.sourceKeyPath isEqualToString:@"createdLocallyAt"]) {
        attributeMapping.propertyValueClass = [NSNumber class];
        *stop = YES;
    }
}];

Custom Value Transformer

- (RKBlockValueTransformer*)timeIntervalSince1970ToDateValueTransformerDouble
{

    if(!_timeIntervalSince1970ToDateValueTransformerDouble){
        _timeIntervalSince1970ToDateValueTransformerDouble = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class destinationClass){

            return ((([inputValueClass isSubclassOfClass:[NSString class]] || [inputValueClass isSubclassOfClass:[NSNumber class]]) && [destinationClass isSubclassOfClass:[NSDate class]]) || ([inputValueClass isSubclassOfClass:[NSDate class]] && ([destinationClass isSubclassOfClass:[NSNumber class]] || [destinationClass isSubclassOfClass:[NSString class]])));

        } transformationBlock:^BOOL(id inputValue,
                                    __autoreleasing id *outputValue,
                                    __unsafe_unretained Class outputClass,
                                    NSError *__autoreleasing *error) {

            static dispatch_once_t onceToken;
            static NSArray *validClasses;
            static NSNumberFormatter *numberFormatter;
            dispatch_once(&onceToken, ^{
                validClasses = @[ [NSNumber class], [NSString class], [NSDate class] ];
                numberFormatter = [NSNumberFormatter new];
                numberFormatter.numberStyle = NSNumberFormatterDecimalStyle;
            });

            RKValueTransformerTestInputValueIsKindOfClass(inputValue, validClasses, error);
            RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputClass, validClasses, error);

            if ([outputClass isSubclassOfClass:[NSDate class]]) {
                if ([inputValue isKindOfClass:[NSNumber class]]) {
                    *outputValue = [NSDate dateWithTimeIntervalSince1970:([inputValue doubleValue] / 1000.0f)];
                } else if ([inputValue isKindOfClass:[NSString class]]) {
                    if ([[inputValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] == 0) {
                        *outputValue = nil;
                        return YES;
                    }
                    NSString *errorDescription = nil;
                    NSNumber *formattedNumber;
                    BOOL success = [numberFormatter getObjectValue:&formattedNumber forString:inputValue errorDescription:&errorDescription];
                    RKValueTransformerTestTransformation(success, error, @"%@", errorDescription);
                    *outputValue = [NSDate dateWithTimeIntervalSince1970:[formattedNumber doubleValue]];
                }
            } else if ([outputClass isSubclassOfClass:[NSNumber class]]) {
                *outputValue = @((long long)([inputValue timeIntervalSince1970] * 1000.0f));
            } else if ([outputClass isSubclassOfClass:[NSString class]]) {
                *outputValue = [numberFormatter stringForObjectValue:@([inputValue timeIntervalSince1970])];
            }
            return YES;

        }];
    }

    return _timeIntervalSince1970ToDateValueTransformerDouble;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants