Restkit dynamic mapping based on previous value - ios

I'm dealing with the following json:
{
"status":
{
"errorCode":{errCode},
"errorMsg":{errMsg},
},
"data":
{
"key1":"value1",
"key2":"value2",
"key3":"value3",
}
}
I need to use different mapping for the object in data, according to errCode value. I tried to use RKDynamicMapping, but got confused with the keyPaths..
Is there any way to achieve that?
Edit:
I'm using this code:
RKObjectMapping *infoMapping = [RKObjectMapping mappingForClass:[Info class]];
[infoMapping addAttributeMappingsFromDictionary:#{#"data.key1":#"key1", #"data.key2":#"key2", #"data.key3:#"key3}];
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
[dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:#"status.errorCode" expectedValue:0 objectMapping:infoMapping]];
[dynamicActivateTravelMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
NSNumber *errorCode = [[representation valueForKey:#"status"] valueForKey:#"errorCode"];
if ([errorCode integerValue] == WSErrorUnknown) {
return unknownMapping;
}
else{
return infoMapping;
}
}];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping method:RKRequestMethodPOST pathPattern:kResource keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:responseDescriptor];
Well, basically this code works, but I have couple of issues with it:
It seems very ugly to keep writing "data.x" for each attribute of the mapping.
The result dictionary comes back with NSNull as the key for the "data" mapping. (its value is fine though..)
status = "<ServerStatusCode: 0x155b2fa0>";
"<null>" = "Info: 0x15535800>";

Technically when you create the dynamic mapping it should be:
[dynamicMapping addMatcher:[RKObjectMappingMatcher matcherWithKeyPath:#"status.errorCode" expectedValue:#0 objectMapping:infoMapping]];
Note the #0, because you should be passing an object (NSNumber), not a plain number (where 0 will equate to nil and another number will cause problems).
The other 2 complaints you have are just the way things are. The first is because you need to index into the data. Both are caused by the keyPath:nil, specifying that the data is accessed from the top level and that there is no key with which to access the result.

Related

Response Descriptor for RestKit JSON Metadata

I have a JSON response that returns me a list of objects, and also a timestamp value as "MetaData". The response looks something like this --
{
"access_time": 1416467865510,
"profiles" : [
{
"user_id": "bbb91ae431b",
"email": "bob#foo.corp",
"first_name": "Bob",
"last_name": "Burroughs",
"primary_phone": "16507001212"
},
{
"user_id": "ddd8d8d8d8d",
"email": "don#foo.corp",
"first_name": "Don",
"last_name": "Darko",
"primary_phone": "14154001212"
}
]
}
My RestKit descriptor code looks something like this. And this is working well, I am getting all objects.
RKEntityMapping *contactMapping = [RKEntityMapping mappingForEntityForName:#"Contact" inManagedObjectStore: managedObjectStore];
[contactMapping addAttributeMappingsFromDictionary:#{
#"user_id" : #"userId",
#"email" : #"email",
#"first_name" : #"firstName",
#"last_name" : #"lastName",
#"primary_phone" : #"primaryPhone"
}];
contactMapping.identificationAttributes = #[#"userId"];
RKResponseDescriptor *contactResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:contactMapping method:RKRequestMethodAny pathPattern:nil keyPath:#"profiles" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
The above thing works well for me.
However, I wanted to have access to the access_time field above too. How do I get access to that? I am thinking of storing that value in NSUserDefaults for later use since it is not a field that is a part of the User/Contact object. How do I do it?
try this:
[contactMapping addAttributeMappingsFromDictionary:#{
#"user_id" : #"userId",
#"email" : #"email",
#"first_name" : #"firstName",
#"last_name" : #"lastName",
#"primary_phone" : #"primaryPhone",
#"#parent.access_time" : #"accessTime",
}];
You can read more here
You could create a relationship between the parent JSON object (that contains "access_time") and the child "profiles" objects with RKRelationshipMapping.
Then instead of having your response descriptor directly accessing the keyPath:#"profiles", you can set it to keyPath:nil and access the whole JSON object including access_time and associated profiles.
You would also need to ensure you had a corresponding Entity and Relationships (in the datamodel) for the parent JSON object (you can call it whatever you like). Then back in the file with the RestKit mappings add the relationships to the parent mapping object with addPropertyMappingsFromArray:.
Then once the request is returned you can iterate through the associated profile objects of the parent JSON (assuming you have XCode create the associated NSManagedObject subclasses) with a simple:
// allObjects returns an NSArray representation of the NSSet
NSArray *profiles = [[parentObject valueForKeyPath:#"profiles"] allObjects];
Hopefully this helps.

Map only on element of a one to many relationships

I'm stuck with the following problem. I have a relationships one_to_many between a Event and Comment. One Event can have many Comment but a Comment has belongs_to only one Event.
Until here, everything is fine. Now, when I'm adding a comment, I would like to map only this new comment. That means I'm using my relationship from Comment to Moment.
I have some troubles with the mapping that I'm not able to solve. My error is at the end of this post after all the description.
I'm receiving this JSON:
"comment": {
"id": 17,
"commentable_id": 12,
"commentable_type": "Moment",
"content": "That's it ! ",
"created_at": "2014-06-20T18:17:42Z",
"updated_at": "2014-06-20T18:17:42Z",
"user_id": 1,
"creator": {
"id": 1,
"email": "test#test.com",
"firstname": "Bobby",
"lastname": "Stouket",
"gender": 0,
"created_at": "2014-04-06T17:48:11Z",
"updated_at": "2014-06-20T18:17:26Z"
}
}
Here is my comment mapping:
RKEntityMapping *commentMapping = [RKEntityMapping mappingForEntityForName:#"Comment" inManagedObjectStore:store];
commentMapping.identificationAttributes = #[ #"commentId"];
[commentMapping addAttributeMappingsFromDictionary:#{
#"id" : #"commentId",
#"updated_at": #"updatedAt",
#"created_at": #"createdAt",
#"user_id": #"userId",
#"commentable_id": #"commentableId",
#"commentable_type": #"commentableType",
#"content": #"content"
}];
RKEntityMapping *userCreatorMapping = [APICallUser RKGetUserMappingOnlyWithAvatarForManagedObjectStore:store];
[commentMapping addConnectionForRelationship:#"creator" connectedBy:#{#"userId": #"userId"}];
[commentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"creator"
toKeyPath:#"creator"
withMapping:userCreatorMapping]];
Here is my code for my moment mapping (with the association with comments which is working) :
RKEntityMapping *momentMapping = [RKEntityMapping mappingForEntityForName:#"Moment" inManagedObjectStore:store];
momentMapping.identificationAttributes = #[ #"momentId"];
[momentMapping addAttributeMappingsFromDictionary:#{
#"id" : #"momentId",
#"creator.id" : #"creatorId",
#"created_at" : #"createdAt",
#"updated_at" : #"updatedAt"
}];
RKEntityMapping *commentMapping = [APICallComment RKGetCommentMappingForManagedObjectStore:store];
[commentMapping addConnectionForRelationship:#"moment" connectedBy:#{#"commentableId":#"momentId"}];
[momentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"comments"
toKeyPath:#"comments"
withMapping:commentMapping]];
There is one more thing to know is that a comment can be on a moment or on a photo. According to my JSON, I don't think I need an RKDynamicMapping but I'm not sure.
Here is the code when I'm using my mapping. The request is send successfully and I receive the JSON written before.
KEntityMapping *commentMapping = [APICallComment RKGetCommentMappingForManagedObjectStore:self.appDelegate.managedObjectStore];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:commentMapping
method:RKRequestMethodPOST
pathPattern:APICallCommentCreateCommentsRouteName
keyPath:#"comment"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[session.objectManager addResponseDescriptor:responseDescriptor];
//session.objectManager.requestSerializationMIMEType=RKMIMETypeJSON;
Error Domain=org.restkit.RestKit.ErrorDomain Code=1001 "No mappable object representations were found at the key paths searched." UserInfo=0xb8a9150 {DetailedErrors=(), NSLocalizedFailureReason=The mapping operation was unable to find any nested object representations at the key paths searched: comments, device, devices
The representation inputted to the mapper was found to contain nested object representations at the following key paths: comment
This likely indicates that you have misconfigured the key paths for your mappings., NSLocalizedDescription=No mappable object representations were found at the key paths searched., keyPath=null}
Edit:
Here is the result of the code line session.objectManager.requestDescriptor. It's really weird. I can see only 1 object in the NSArray. When I print it I can read:
Printing description of $1:
<__NSArrayI 0xbd61010>(
<RKRequestDescriptor: 0xbd12bb0 method=(POST) objectClass=BasicLocation rootKeyPath=position : <RKObjectMapping:0xbd40b70 objectClass=NSMutableDictionary propertyMappings=(
"<RKAttributeMapping: 0xbd545d0 latitude => lat>",
"<RKAttributeMapping: 0xbd58430 longitude => lng>"
)>>
)
Nowhere I've written that positionshould be the rootKeyPath and my other attributes are not here (content, commentableType, userId, createdAt, updatedAt, commentId).
Thank you for your help.
You create:
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:commentMapping
method:RKRequestMethodPOST
pathPattern:APICallCommentCreateCommentsRouteName
keyPath:#"comment"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
but you can't ever add it to the object manager, because it only understands comments, device, devices.
That would seem to be your main issue.
You wouldn't usually do this:
[commentMapping addConnectionForRelationship:#"creator" connectedBy:#{#"userId": #"userId"}];
[commentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"creator"
toKeyPath:#"creator"
withMapping:userCreatorMapping]];
because you are supplying 2 different mappings for exactly the same content and relationship where you only need one because the user information is nested inside the comment information. So, you can remove the foreign key mapping (addConnectionForRelationship:).
The mapping was good but the mistake comes from here:
[RKResponseDescriptor responseDescriptorWithMapping:commentMapping
method:RKRequestMethodGET
pathPattern:HERE
keyPath:#"comment"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
I didn't write the path pattern. This variable changed and everything works perfectly.

Using RKDynamicMapping on Foursquare's Lists API to capture lists

I'm looking to get all of my Foursquare Lists into Core Data. I'd like to use Restkit to accomplish this. The structure of the /v2/users/self/lists response is:
"response": {
"lists": {
"count": 8,
"groups": [
{
"type": "created",
"name": "Lists You've Created",
"count": 6,
"items": [
{
"id": "13250/todos",
"name": "My to-do list", ...
}
{
"id": "13251/something",
"name": "Some List", ...
},
{
"id": "13252/somethingelse",
"name": "Some Other List", ...
}
]
},
{
"type": "followed",
"name": "Lists You've Saved",
"count": 1,
"items": [
{
"id": "5105e3cae4b0e721ca7b400a",
"name": "Portland's Best Coffee - 2012", ...
}
{
...
}
]
}
]
}
As you can see there are 2 lists under the keyPath response.lists.groups. Ultimately I'd like to merge those 2 lists into 1, but I'd be happy with getting 2 separate lists.
I've set up my mappings as follows:
RKEntityMapping* listMapping = [RKEntityMapping mappingForEntityForName:[FOFSList entityName]
inManagedObjectStore:objectManager.managedObjectStore];
[listMapping addAttributeMappingsFromDictionary:#{
#"id": #"listID",
#"title": #"name",
#"description": #"desc",
#"user": #"user",
#"following": #"following",
#"collaborative": #"collaborative",
#"canonicalUrl": #"canonicalUrl",
#"venueCount": #"venueCount",
#"visitedCount": #"visitedCount"
}];
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
[listMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:nil
toKeyPath:#"items"
withMapping:dynamicMapping]];
RKResponseDescriptor *listResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:listMapping
method:RKRequestMethodGET
pathPattern:nil
keyPath:#"response.lists.groups"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:listResponseDescriptor];
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
if ([[representation valueForKey:#"type"] isEqualToString:#"created"]) {
return listMapping;
} else if ([[representation valueForKey:#"type"] isEqualToString:#"followed"]) {
return listMapping;
}
return nil;
}];
listMapping.identificationAttributes = #[ #"listID" ];
I end up with an error:
* Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[ valueForUndefinedKey:]: this class is not key value coding-compliant for the key propertyMappings.'
Am I supposed to be using RKDynamicMappings? Is there some trick that I'm missing for parsing a response that is styled like this?
For those that are interested, I got a little bit creative with the RKResponseDescriptor
RKResponseDescriptor *listResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:listMapping
method:RKRequestMethodGET
pathPattern:nil
keyPath:#"response.lists.groups.#distinctUnionOfArrays.items"
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
See the collection operation #distinctUinionOfArrays was ultimately what got me what I needed. It makes a union of the 2 groups arrays, then I grab the items key from each of the objects in the union of the arrays.
At the moment the dynamic mapping is serving as a filter on the type. That's fine if it's what you want and is the correct way to achieve the filter. But, you're applying that mapping in the wrong way.
The dynamic mapping should be supplies as the mapping for the response descriptor. It analyses the incoming object and returns the appropriate mapping to apply.
You need a new, non-dynamic, mapping to handle the nested items.
Your other question about merging can't be handled during the mapping, but it could be done by adding a method to the destination class which is called with the mapped array and it merges with an existing array and pushes the merged result into the true instance variable. The mapping destination would be the method instead of the instance variable.

Reskit - Mapping to array

I'm trying to run some unit tests to test my mappings with RestKit v0.20, however I am getting an error that my destination object is nil. I have track this down to the fact that the mapping is failing because the sourceType is an NSArray and my destinationType is an NSNumber. I think this is because my mapping keypaths are incorrect. I am trying to map the songCard JSON to my objet. I have included my JSON and mapping test below.
It Would be great it someone could help me to set the correct keypath.
{"status" : 2000,
"content" : {
"cardList" : [
{
"songCard" : {
"likes" : 2,
"dislikes" : 3
}
}
]
},
"message" : "OK"
}
Unit Test class
- (RKObjectMapping *)songMetadataMapping
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[SongMetadata class]];
[mapping addAttributeMappingsFromDictionary:#{
#"content.cardList.songCard.likes": #"likes"
}];
return mapping;
}
- (void)testSongMetadataMapping
{
NSString *parsedJSON = [RKTestFixture parsedObjectWithContentsOfFixture:#"songMetadata.json"];
RKMappingTest *test = [RKMappingTest testForMapping:[self songMetadataMapping] sourceObject:parsedJSON destinationObject:nil];
[test addExpectation:[RKPropertyMappingTestExpectation expectationWithSourceKeyPath:#"content.cardList.songCard.likes" destinationKeyPath:#"likes" value:#"2"]];
STAssertTrue([test evaluate], #"Mappings failed");
}
UPDATE
After further debugging I have found that the value 2 in my JSON string is being evaluated as an NSArray, when this should be evaluated as NSNumber. As a quick test I removed the [ ] in my JSON and the value 2 was correctly evaluated as an NSNumber. This doesn't solve my problem though as I have need to identify my JSON as an array of songCard objects
As you have noticed, you cannot use the keypath as you have specified when an array is in play. I can think of two options - the first is a long shot, but does the key path content.cardList[0].songCard.likes work?
Otherwise, consider using the method:
+ (instancetype)expectationWithSourceKeyPath:(NSString *)sourceKeyPath
destinationKeyPath:(NSString *)destinationKeyPath
evaluationBlock:(RKMappingTestExpectationEvaluationBlock)evaluationBlock;
With keypath content.cardList and supplying an evaluation block that 1) checks that the mapping is an array that contains a single object. You can then check that the object contains a songCard object and that has a likes value of 2.

How to map a JSON array with RestKit

I have json string like this format :
[{"image":"/0001.jpg","link":"/index.php"},
{"image":"/0001.jpg","link":"/index.php"}]
it does not have a key in the top level.
[mapping mapKeyPath:#"image" toAttribute:#"image"];
mapping like this won't work , it give me the error:
restkit.object_mapping:RKObjectMapper.m:81 Adding mapping error: Could not find an object mapping for keyPath: ''
How to map this type of json ?
Use
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:myObject];
You should check "Mapping without KVC" section on Restkit Object Mapping Docs.
Here is an example from the docs:
[
{ "title": "RestKit Object Mapping Intro",
"body": "This article details how to use RestKit object mapping...",
"author": {
"name": "Blake Watters",
"email": "blake#restkit.org"
},
"publication_date": "7/4/2011"
}
]
And you map that like this:
// Our familiar articlesMapping from earlier
RKObjectMapping* articleMapping = [RKObjectMapping mappingForClass:[Article class]];
[articleMapping mapKeyPath:#"title" toAttribute:#"title"];
[articleMapping mapKeyPath:#"body" toAttribute:#"body"];
[articleMapping mapKeyPath:#"author" toAttribute:#"author"];
[articleMapping mapKeyPath:#"publication_date" toAttribute:#"publicationDate"];
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:articleMapping];
For me the solution was:
add this response descriptor, use the object inside de array
[manager addResponseDescriptor:[ObjectInsideTheArray getResponseDescriptor]];
and in the success response
NSArray *response = (NSArray *)[mappingResult array]; //instead of firstObject

Resources