RestKit custom RKValueTransformer not being run for NSDate property - ios

I would like to run a custom RKValueTransformer on an NSDate property when creating a specific request. I'm happy (and would prefer to) use the default in all other scenarios. My understanding is that I can set up the mapping, specify a value transformer and it'll use that, only falling back to the defaults if no value transformer is set. I'm running RestKit 0.23.3.
I have the following mapping set up for my request (I've obviously trimmed a bunch from this):
#property (nonatomic, retain) NSDate * dueDate;
...
+(RKEntityMapping *) createRequestMapping
{
static RKEntityMapping* map = nil;
if (map == nil)
{
map = (RKEntityMapping *)[RKObjectMapping requestMapping];
[map addAttributeMappingsFromDictionary:...];
RKValueTransformer *dueDateValueTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class inputValueClass, __unsafe_unretained Class outputValueClass) {
return [inputValueClass isSubclassOfClass:[NSDate class]];
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, __unsafe_unretained Class outputClass, NSError *__autoreleasing *error) {
*outputValue = [[self dueDateRequestFormatter] stringFromDate:inputValue];
return YES;
}];
RKAttributeMapping *dueDateMapping = [RKAttributeMapping attributeMappingFromKeyPath:#"dueDate" toKeyPath:#"due_date"];
dueDateMapping.valueTransformer = dueDateValueTransformer;
[map addPropertyMapping:dueDateMapping];
}
return map;
}
When my test for this runs, I can see that the mapping is used (since the target key path "due_date" exists in the dictionary), but the value transformer is not - rather, it appears the default NSDate transformer is used:
XCTAssertEqualObjects(#"2014-05-01", [item objectForKey:#"due_date"]);
-[RestKitCreateMappingTests testCreateSequenceMappings] failed: ((#"2014-05-01") equal to ([item objectForKey:#"due_date"])) failed: ("2014-05-01") is not equal to ("2014-05-01 00:00:00 +0000")
I've set breakpoints in both the validation and transformation blocks, and neither is hit.
Why is RestKit not using my value transformer?

I had to specify the propertyValueClass so that RestKit knew what type of target property to map to:
RKAttributeMapping *dueDateMapping = [RKAttributeMapping attributeMappingFromKeyPath:#"dueDate" toKeyPath:#"due_date"];
dueDateMapping.valueTransformer = dueDateValueTransformer;
dueDateMapping.propertyValueClass = [NSString class];
[map addPropertyMapping:dueDateMapping];
Without this, the following check failed in the mapping operation, and it never got a chance to use the custom value transformer, rather it just outputs the input value:
Class transformedValueClass = propertyMapping.propertyValueClass ?: [self.objectMapping classForKeyPath:propertyMapping.destinationKeyPath];
if (! transformedValueClass) {
*outputValue = inputValue;
return YES;
}
BOOL success = [propertyMapping.valueTransformer transformValue:inputValue toValue:outputValue ofClass:transformedValueClass error:error];
return success;

Related

Boolean value in the NSDictionary serialized as true/false for x-www-form-urlencoded

RestKit version: 0.27.0
Is there any way how to let RestKit to serialize the boolean value stored in NSDictionary to x-www-form-urlencoded parameters?
I have NSDictionary filled with values, it is propagated to the encoded string:
product=ACCOUNT&rejected=1&type=NUMBER_OF_LOGINS_MONTH
But I need true/false instead of 1 or 0 as the value for rejected parameter.
Is there any way how to use class specification similar to RKAttributeMapping's propertyValueClass to override the default type NSNumber? Also NSNumber is internally implemented as __NSCFBoolean. Is it possible to use this information to serialize the value as true/false?
I have subclassed RKURLEncodedSerialization and copied the implementation from RKAFQueryStringFromParametersWithEncoding:
static NSString * TSNRKAFQueryStringFromParametersWithEncoding(NSDictionary *parameters, NSStringEncoding stringEncoding) {
NSMutableArray *mutablePairs = [NSMutableArray array];
for (TSNRKAFQueryStringPair *pair in TSNRKAFQueryStringPairsFromDictionary(parameters)) {
if([NSStringFromClass([pair.value class]) isEqualToString:#"__NSCFBoolean"]) {
pair.value = TSNNSCFBooleanToBooleanStringConvertor(pair.value);
}
[mutablePairs addObject:[pair URLEncodedStringValueWithEncoding:stringEncoding]];
}
return [mutablePairs componentsJoinedByString:#"&"];
}
NSString* TSNNSCFBooleanToBooleanStringConvertor(NSNumber* booleanInNSNumber) {
return [booleanInNSNumber boolValue] ? #"true" : #"false";
}
Also remember to copy the other methods/objects and rename them otherwise it will collide when being linked.
The new subclass has to be registered with the RestKit:
[RKMIMETypeSerialization unregisterClass:[RKURLEncodedSerialization class]];
[RKMIMETypeSerialization registerClass:[TSNRKURLEncodedSerialization class] forMIMEType:RKMIMETypeFormURLEncoded];
UPDATE:
This solution works for requests with all methods except GET, HEAD and DELETE. These methods still trigger the original serialisation: not true/false, but 1/0.
You can convert a boolean NSNumber to a string with
NSNumber *number = #YES;
NSString *boolString = number.boolValue ? #"true" : #"false";
NSLog(#"%#", boolString);

Why core data won't trigger my value transformer?

I'm try to store CCLocation in CoreData.
I'm on beginning of write my custom value transformer, but i'm stuck.
My transformer:
#import "LocationToDataTransformer.h"
#import <CoreLocation/CoreLocation.h>
#implementation LocationToDataTransformer
+ (BOOL)allowsReverseTransformation {
return YES;
}
+ (Class)transformedValueClass {
NSLog(#"tranform");
return [NSData class];
}
- (id)transformedValue:(id)value {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:value];
NSLog(#"tranform");
return data;
}
- (id)reverseTransformedValue:(id)value {
CLLocation *location = (CLLocation*)[NSKeyedUnarchiver unarchiveObjectWithData:value];
NSLog(#"tranform");
return location;
}
#end
And transformer registration:
+ (void)initialize {
LocationToDataTransformer *transformer = [[LocationToDataTransformer alloc] init];
[NSValueTransformer setValueTransformer:transformer forName:#"LocationToDataTransformer"];
}
I've check [NSValueTransformer valueTransformerNames] array and i see my transformer name in it.
In model I have a entity with 3 attributes:
title (string)
subtitle (string)
location (transformable) - value transformer
name:LocationToDataTransformer
And when I'm saving my location, nothing happens, other attributes of entity (strings) saves, but location is nil.
Saving entity code:
-(void)saveBookmark:(BookmarkEntity*)bookmark {
NSManagedObject *object = [[NSManagedObject alloc]initWithEntity:_descr insertIntoManagedObjectContext:_context];
BookmarkEntity* entity = (BookmarkEntity*)object;
entity.title = bookmark.title;
entity.subtitle = bookmark.subtitle;
entity.location = bookmark.location;
if (_context.hasChanges) {
NSError* error;
bool saved = [_context save:&error];
if (!saved) {
NSLog(#"Error while saving: %#",error.localizedDescription);
}
}
}
As you can see i've added NSLog's to my transformer, but i not see it in logs. Also i try breakpoints, and no one method from my transformer don't calls.
I even try to set transformer name to "abracadabra" and see nothing in logs, nothing about wrong value transformer name.
I'm totally confused. Anyone have an idea where i missed up? Thanks.
Your transformer is the wrong way around.

How to use a RKValueTransformer to convert a NSString to a Core Data NSManagedObject

I'm getting a string back from a server, and I need to convert it into a State object, which is a subclass of NSManagedObject. The transformer works fine, except that I don't know what the ManagedObjectContext is at the time of the mapping, so I get the error illegal attempt to establish a relationship 'state' between objects in different contexts.
RKEntityMapping knows how to automatically create the proper object during the mapping operation, so the functionality is within RestKit. How can I create this core data object properly?
RKValueTransformer *stateTransformer = [RKBlockValueTransformer valueTransformerWithValidationBlock:^BOOL(__unsafe_unretained Class sourceClass, __unsafe_unretained Class destinationClass) {
return ([sourceClass isSubclassOfClass:[NSString class]] && [destinationClass isSubclassOfClass:[State class]]);
} transformationBlock:^BOOL(id inputValue, __autoreleasing id *outputValue, Class outputValueClass, NSError *__autoreleasing *error) {
// Validate the input and output
RKValueTransformerTestInputValueIsKindOfClass(inputValue, [NSString class], error);
RKValueTransformerTestOutputValueClassIsSubclassOfClass(outputValueClass, [State class], error);
State *state = [NSEntityDescription insertNewObjectForEntityForName:#"State" inManagedObjectContext:managedObjectStore.mainQueueManagedObjectContext];
state.abbreviation = inputValue;
*outputValue = state;
return YES;
}];
It looks like you're creating your state object and setting the string into a single attribute on the object. To do this you don't need a custom transformer, you just need to use a nil key path mapping.
See the Mapping Values without Key Paths section of the Object-mapping wiki page.

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]
I had exactly the same issue - NSData of image content <-> NSString BASE64 encoded for the Rest call. I got the outbound working pretty quickly as you did, but the incoming mapping was a little trickier.
I raised this issue: https://github.com/RestKit/RestKit/issues/1949 and through some working through the problem, discovered that you need to set the propertyValueClass on the RKPropertyMapping in order to get RestKit to recognize that you want to turn the NSData into a NSString. Once this is done, you get the mapping done for you.
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]).
After a deep debug at [self transformValue:value toValue:&transformedValue withPropertyMapping:attributeMapping error:&error], I find the valueTransformer I added is not at the head but the second of all valueTransformer in defaultValueTransformer.
The first valueTransformer is a RKISO8601DateFormatter, by search this key word, I find it is inserted to the head of defaultValueTransformer in [RKObjectMapping initialize].
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Add an ISO8601DateFormatter to the transformation stack for backwards compatibility
RKISO8601DateFormatter *dateFormatter = [RKISO8601DateFormatter defaultISO8601DateFormatter];
[[RKValueTransformer defaultValueTransformer] insertValueTransformer:dateFormatter atIndex:0];
});
}
The problem is that my valueTransformer is inserted before the [RKObjectMapping initialize] method get called. In another word, RKISO8601DateFormatter is inserted to the head after mine which made my valueTransformer the second one.
My solution is quite simple, just call [RKObjectMapping new] before my insert code.
BTW, you should always give a name to your valueTransformer thus it could be quickly recognized when debug.

How to get inserted and updated objects from NSPersistentStoreDidImportUbiquitousChangesNotification?

I want to get the inserted and the update objects from NSPersistentStoreDidImportUbiquitousChangesNotification to do some check on them.
Objects can be of two kind of classes: "Alpha" and "Beta". Both classes have the
property (nonatomic, retain) NSString* name
which is the one I should check.
How do I get it?
The following code doesn't work because it says "name" is an unknown selector:
-(void) checkObjects
{
NSDictionary *insertedObjects = [[note userInfo] objectForKey: #"inserted"];
NSDictionary *updatedObjects = [[note userInfo] objectForKey: #"updated"];
for(NSManagedObject *obj in insertedObjects){
if([obj.entity.managedObjectClassName isEqualToString:#"Alpha"]){
Alpha *alpha = (Alpha*) obj;
if (alpha.name isEqualToString:#"xyz"){
//Do some check
}
}else if([obj.entity.managedObjectClassName isEqualToString:#"Beta"]){
Beta *beta = (Beta*) obj;
if (beta.name isEqualToString:#"xyz"){
//Do some check
}
}
}
}
If I change:
Alpha *alpha = (Alpha*) obj;
Beta *beta = (Beta*) obj;
To:
Alpha *alpha = (Alpha*) obj.entity;
Beta *beta = (Beta*) obj.entity;
alpha = Alpha <-- It is the name of the class, not of the object I want!
beta = Beta <--- It is the name of the class, not of the object I want!
When you get NSPersistentStoreDidImportUbiquitousContentChangesNotification, the objects in userInfo are not managed objects, they're managed object IDs. That is, instances of NSManagedObjectID. If you want to look up attributes on the managed object, you need to get the object corresponding to the ID. Something like
NSDictionary *insertedObjectIDs = [[note userInfo] objectForKey:NSInsertedObjectsKey];
for(NSManagedObjectID *objID in insertedObjects) {
NSError *error = nil;
NSManagedObject *obj = [self.managedObjectContext existingObjectWithID:objID error:&error];
....continue...
}
You may need to change that if self doesn't have a managed object context.
Also, on a slight tangent-- it's generally better to use NSInsertedObjectsKey instead of #"inserted" and NSUpdatedObjectsKey instead of #"updated". Apple probably won't change the key names, but they could, so using the key names instead of string literals is a better choice.

Resources