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.
Related
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.
I'm trying to map an API result that has dynamic nested attributes. The result (json) is I have is this:
{
"url": "https://api.github.com/gists/8901308",
"id": "8901308",
"html_url": "https://gist.github.com/8901308",
"files": {
"node-and-npm-in-30-seconds.sh": {
"filename": "node-and-npm-in-30-seconds.sh",
"type": "application/sh",
"language": "Shell",
"raw_url": "https://gist.github.com/braincrash/8901308/raw/bae861f7c4ab0c1ffd9962439e770b02f52c5dd7/node-and-npm-in-30-seconds.sh",
"size": 352
},
"only-git-all-the-way.sh": {
"filename": "only-git-all-the-way.sh",
"type": "application/sh",
"language": "Shell",
"raw_url": "https://gist.github.com/braincrash/8901308/raw/eba9667b37218ffb41892411c94abd051b0e269a/only-git-all-the-way.sh",
"size": 440
}
}
}
I can get all the attributes, but the files won't work. Here's my mapping:
RKEntityMapping *fileMapping = [RKEntityMapping mappingForEntityForName:#"File" inManagedObjectStore:managedObjectStore];
fileMapping.forceCollectionMapping = YES;
[fileMapping addAttributeMappingFromKeyOfRepresentationToAttribute:#"filename"];
[fileMapping addAttributeMappingsFromDictionary:#{#"(filename).raw_url": #"rawURL",
#"(filename).size": #"size"}];
RKEntityMapping *gistMapping = [RKEntityMapping mappingForEntityForName:#"Gist" inManagedObjectStore:managedObjectStore];
[gistMapping addAttributeMappingsFromDictionary:#{
#"id": #"gistID",
#"url": #"jsonURL",
#"html_url": #"htmlURL"}];
gistMapping.identificationAttributes = #[ #"gistID" ];
[gistMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"files" toKeyPath:#"files" withMapping:fileMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:gistMapping method:RKRequestMethodGET pathPattern:#"/gists/public" keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
When I look at the File inside the Gist object, I only get the filename, but not the url nor size:
filename = "radiation.js";
gist = "0xb9742f0 <x-coredata://D37E442D-45BA-4A0E-B7A5-A349F75FA362/Gist/p21>";
rawURL = nil;
size = 0;
Thanks for the help!!
Your file names contain dots so they will mess up key path navigation.
RestKit does struggle with this a little and I don't think there is currently a solution. Some have been proposed (https://github.com/RestKit/RestKit/pull/1541/files) but I think it remains an open issue.
The problem is how to know whether a dot represents a key path to the traversed or not. In theory it is possible to determine but it isn't necessarily trivial to implement in a framework or performant. You may be able to debug around the usage of RKObjectMappingNestingAttributeKeyName and add a custom solution just for that part.
{
"type": "at or leave",
"time": "XXXX",
"place_name": "Xx",
"place_id": "xx",
"place_attributes": {
"key": "val",
"key2": "val2",
},
"place_type": "public or private"
}
i want to post json like above.But placeAttributes dictionary in my app will be having unknown number of keys which are needed to be mapped with "place_attributes" in json above.
Created a NSDictionary* placeAttributes property in my request mapping class, mapped it to place_attributes key in json and directly assigned my dictionary to placeAttributes.
RKObjectMapping* map = [RKObjectMapping requestMapping];
[map addAttributeMappingsFromDictionary:#{
#"type":#"type",
#"time":#"time",
#"place_name":#"place_name",
#"place_id":#"place_id",
#"place_type":#"place_type",
#"placeAttributes":#"place_attributes"
}];
I have to map this structure, but I don't know how to identify the "br.myservice.com" part, because it changes and I cannot build a regular RKObjectMapping, as it uses fixed strings.
{ "objects": {
"br.myservice.com": {
"url": "http://br.myservice.com",
"name": "Brazil",
"flag": "br",
"countries": ["br"]
},
"us.myservice.com": {
"url": "http://us.myservice.com",
"name": "United States",
"flag": "us",
"countries": ["us"]
}
}
You need to use addAttributeMappingFromKeyOfRepresentationToAttribute: like so:
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[MyObject class]];
mapping.forceCollectionMapping = YES;
[mapping addAttributeMappingFromKeyOfRepresentationToAttribute:#"host"];
[mapping addAttributeMappingsFromDictionary:#{
#"(host).url": #"url",
#"(host).name": #"name",
... and so on ....
}];
Assuming that the keys are unknown to you at the time of writing the code:
You would need to create a dynamic mapping (RKDynamicMapping) with a block which inspects the received JSON dictionary and creates (and returns) a custom object mapping on the fly.
If you know the keys, just add them all and any that are appropriate will be used at runtime.
this is what my sample code looks like:
{
"name": "Ahmad Mansour",
"subjects": [
{
"parent_subject_name": "Arabic",
"subject_name": "Shafahi",
"exams": [
{
"score": "30.00",
"exam_name": "Sa3i 1 "
},
{
"score": "50.00",
"exam_name": "sa3i 2"
},
{
"score": "100.00",
"exam_name": "First Semester Exam"
}
]
},
{
"parent_subject_name": "Arabic",
"subject_name": "Khati",
"exams": [
{
"score": "50.00",
"exam_name": "Sa3i 1 "
},
{
"score": "60.00",
"exam_name": "sa3i 2"
},
{
"score": "95.00",
"exam_name": "First Semester Exam"
}
]
},
for the subject entity.. my mapping works just fine:
RKEntityMapping *subjectEntityMapping = [RKEntityMapping mappingForEntityForName:kSubjectIdentity inManagedObjectStore:model.managedObjectStore];
[subjectEntityMapping addAttributeMappingsFromDictionary:#{ #"subject_name": #"name",
#"parent_subject_name" :#"parent_name"
}];
subjectEntityMapping.identificationAttributes = #[ #"name" ];
RKResponseDescriptor *studentResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:subjectEntityMapping pathPattern:kGradesPath keyPath:#"subjects" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[model.objectManager addResponseDescriptor:studentResponseDescriptor];
but when i do the exam score mapping.. things blow up:
RKEntityMapping *examEntityMapping = [RKEntityMapping mappingForEntityForName:kExamIdentity inManagedObjectStore:model.managedObjectStore];
[examEntityMapping addAttributeMappingsFromDictionary:#{ #"exams.exam_name": #"name" }];
examEntityMapping.identificationAttributes = #[ #"name" ];
RKResponseDescriptor *examResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:examEntityMapping pathPattern:kGradesPath keyPath:#"subjects" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[model.objectManager addResponseDescriptor:examResponseDescriptor];
i get the following error:
E restkit.object_mapping:RKMappingOperation.m:431 Failed transformation of value at keyPath 'exams.exam_name' to representation of type 'NSString': Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3002 "Failed transformation of value '(
"Sa3i 1 ",
"sa3i 2",
"First Semester Exam"
)' to NSString: none of the 2 value transformers consulted were successful." UserInfo=0x9a508a0 {detailedErrors=(
"Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3002 \"The given value is not already an instance of 'NSString'\" UserInfo=0x9a50800 {NSLocalizedDescription=The given value is not already an instance of 'NSString'}",
"Error Domain=org.restkit.RKValueTransformers.ErrorDomain Code=3000 \"Expected an `inputValue` of type `NSNull`, but got a `__NSArrayI`.\" UserInfo=0x9a50830 {NSLocalizedDescription=Expected an `inputValue` of type `NSNull`, but got a `__NSArrayI`.}"
I also tried this mapping, but i get the exact same error:
[examEntityMapping addAttributeMappingsFromDictionary:#{ #"exam_name": #"name" }];
examEntityMapping.identificationAttributes = #[ #"name" ];
RKResponseDescriptor *examResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:examEntityMapping pathPattern:kGradesPath keyPath:#"subjects.exams" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
ideas?
You shouldn't have a response descriptor for exams. It's nested data in your subject so it should be mapped using a relationship on the subject mapping.
Using the response descriptor doesn't work because you're trying to map into 2 arrays when the mapping only caters for one. Hence you get an error when RestKit tries to convert an array into a string.
Also, for your exam mapping you should probably specify multiple attributes for the unique identity as exam names are used repeatedly for different instances...