I am implementing a simple iOS app that consumes reddit feeds and I need to show the feed comments. I am using RESTKit to do the ORM part and I am having an annoyance with mapping the comments.
Reddit comments can have replies and the replies can have replies and so on. It's a tree of comments. This is no big deal. The problem is that among the children nodes there might exist one that's different (used to sign that there are more comments to load on that particular thread).
Here's a JSON example:
"replies":{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"approved_by": null,
"author": "InstaRamen",
"author_flair_css_class": null,
"author_flair_text": null,
"banned_by": null,
"body": "that or he counted it all wrong",
"body_html":"<div class=\"md\"><p>that or he counted it all wrong</p>\n</div>",
"controversiality": 0,
"created": 1405093511,
"created_utc": 1405064711,
"distinguished": null,
"downs": 0,
"edited": false,
"gilded": 0
"id": "ciubfdk",
"likes": null,
"link_id": "t3_2ae8oa",
"name": "t1_ciubfdk",
"num_reports": null,
"parent_id": "t1_ciuao19",
"replies":
{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"approved_by": null,
"author": "Everlasting",
"author_flair_css_class": null,
"author_flair_text": null,
"banned_by": null,
"body": "3, sir.",
"body_html":"<div class=\"md\"><p>3, sir.</p>\n</div>",
"controversiality": 0,
"created": 1405093986,
"created_utc": 1405065186,
"distinguished": null,
"downs": 0,
"edited": false,
"gilded": 0,
"id": "ciubj8h",
"likes": null,
"link_id": "t3_2ae8oa",
"name": "t1_ciubj8h",
"num_reports": null,
"parent_id": "t1_ciubfdk",
"replies":
{
"data":
{
"after": null,
"before": null,
"children":
[
{
"data":
{
"children":
[
"ciublgq"
],
"count": 0,
"id":"ciublgq",
"name":"t1_ciublgq",
"parent_id": "t1_ciubj8h"
},
"kind": "more"
}
],
"modhash": ""
},
"kind": "Listing"
},...
So, you can see that the typical comment replies has a structure of
replies
|-data
|-children
|-data
|- #comment key/values
But the "Load more" item has a structure of:
replies
|-data
|-children
|-data
|-children
|-more loading info...
My DataObject is:
#interface RedditComment : NSObject
#property (nonatomic, copy) NSString *author;
#property (nonatomic, readwrite) NSInteger createdUTC;
#property (nonatomic, readwrite) NSInteger score;
#property (nonatomic, copy) NSString *subreddit;
#property (nonatomic, copy) NSString *subredditId;
#property (nonatomic, readwrite) NSInteger ups;
#property (nonatomic, copy) NSString *body;
#property (nonatomic, copy) NSArray *replies;
+ (RKObjectMapping*)restKitMapping;
#end
And my mapping implementation is:
#implementation RedditComment
+ (RKObjectMapping*)restKitMapping;
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]];
[mapping addAttributeMappingsFromDictionary:#{
#"author": #"author",
#"created_utc": #"createdUTC",
#"score": #"score",
#"subreddit": #"subreddit",
#"subreddit_id": #"subredditId",
#"body": #"body",
#"ups": #"ups",
}];
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"replies" toKeyPath:#"replies" withMapping:mapping]];
return mapping;
}
#end
This yields an error:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFConstantString 0x1daa0d0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key body.'
This is because the "Load more" node doesn't have a body key (it's missing other keys as well, but this is the first one that RESTKit complains about).
I've tried to use a dynamic mapping block to change the mapping in runtime but couldn't figure out how to isolate the problematic node (i.e. the "Load More").
My question is how can I distinguish between both possible values in "replies" JSON nodes?
Can I have two mappings for the same partial key?
That is a mapping for replies > data > children > data and another to replies > data > children > data > children. If so how to do it?
Or is there anything wrong in my line of thinking?
Thanks in advance to everyone that is able to help or that simply take the time to read... ;-)
EDIT
including response descriptor
As requested on comments here's the RKResponseDescriptor:
RKResponseDescriptor *commentResponseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[PPRedditComment restKitMapping]
method:RKRequestMethodAny
pathPattern:nil
keyPath:#"data"
statusCodes:nil];
EDIT #2
including a "good comment" node to show what I am interested to map to my DO
"data":
{
"approved_by": null,
"author": "PerimeterBlue",
"author_flair_css_class":null,
"author_flair_text":null,
"banned_by":null,
"body": "This actually looks really cool.",
"body_html": "<div class=\"md\"><p>This actually looks really cool.</p>\n</div>",
"controversiality":0,
"created": 1405095679,
"created_utc": 1405066879,
"distinguished":null,
"downs": 0,
"edited": false,
"gilded": 0,
"id": "ciubwld",
"likes":null,
"link_id": "t3_2aelr4",
"name": "t1_ciubwld",
"num_reports": null,
"parent_id": "t3_2aelr4",
"replies": {...},
"saved": false,
"score": 48,
"score_hidden": false,
"subreddit": "gaming",
"subreddit_id": "t5_2qh03",
"ups": 48
}
Ok, apparently got the issue. Wain was right: it was a path related issue.
There were certain nodes which was supposed to contain dictionaries and sometimes contained strings. In the way the paths were defined this caused an non-existent selector (the property from DO) to be called on the NSString class. Since it didn't contain it, it went BOOM!
Mapping was corrected to this:
+ (RKObjectMapping*)restKitMapping;
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[self class]];
[mapping addAttributeMappingsFromDictionary:#{
#"author": #"author",
#"created_utc": #"createdUTC",
#"score": #"score",
#"subreddit": #"subreddit",
#"subreddit_id": #"subredditId",
#"body": #"body",
#"ups": #"ups",
}];
RKDynamicMapping *dynamicMapping = [RKDynamicMapping new];
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
RKObjectMapping *result = mapping;
// When node is empty string don't map...
if([representation isKindOfClass:[NSString class]] && [representation isEqualToString:#""])
{
result = nil;
}
return result;
}];
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"replies"
toKeyPath:#"replies"
withMapping:dynamicMapping]];
return mapping;
}
Response descriptor corrected to:
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:[PPRedditFeedCollection restKitMapping]
method:RKRequestMethodAny
pathPattern:nil
keyPath:#"data"
statusCodes:nil];
Hope this helps someone! :-)
Related
I am having some difficulties POSTing an NSManagedObject with nested NSManagedObjects with RestKit. I appear to be getting duplicate records for the sub-NSManagedObjects inserted into CoreData when the POST returns. Here is a snapshot of the model:
Here is the JSON that I am POSTing:
{
"actions": [],
"application": "Identify",
"createBy": "welcomed",
"createDt": "2014-04-11T16:26:15Z",
"description": null,
"externalId": null,
"groupId": "5",
"id": 0,
"images": [
{
"format": "JPEG",
"height": 200,
"id": 0,
"image": "/9j/4A..../Pv5n/9k=",
"status": "C",
"type": "MUGSHOT",
"width": 200
}
],
"locked": null,
"modifyBy": null,
"modifyDt": null,
"priv": null
}
Here is the JSON that is returned from the service after the POST:
{
"actions": [],
"application": "Identify",
"createBy": "welcomed",
"createDt": 1397233575000,
"description": null,
"externalId": null,
"groupId": "5",
"id": 11,
"images": [
{
"captureDevice": null,
"createBy": null,
"createDt": null,
"format": "JPEG",
"height": 200,
"id": 11,
"image": "/9j/4AAQSkZJR.../Pv5n/9k=",
"recordId": 11,
"status": "C",
"type": "MUGSHOT",
"width": 200
}
],
"locked": false,
"modifyBy": null,
"modifyDt": null,
"priv": false
}
EDIT (I suppose this is important): Here is the mapping for the WTSImage and WTSRecord:
RKEntityMapping *recordMapping = [RKEntityMapping mappingForEntityForName:#"WTSRecord" inManagedObjectStore:self.managedObjectStore];
[recordMapping addAttributeMappingsFromDictionary:#{
#"id":#"dbId",
#"externalId":#"extId",
#"groupId":#"groupId",
#"application": #"application",
#"description": #"desc",
#"priv": #"priv",
#"locked": #"locked",
#"createBy": #"createBy",
#"createDt": #"createDt",
#"modifyBy": #"modifyBy",
#"modifyDt": #"modifyDt",
}];
recordMapping.identificationAttributes = #[#"dbId"];
//image mapping
RKEntityMapping *imageMapping = [RKEntityMapping mappingForEntityForName:#"WTSImage" inManagedObjectStore:self.managedObjectStore];
[imageMapping addAttributeMappingsFromDictionary:#{
#"id": #"dbId",
#"status": #"status",
#"type": #"type",
#"format": #"format",
#"width": #"width",
#"height": #"height",
#"image": #"base64Image"
}];
imageMapping.identificationAttributes = #[#"dbId"];
[recordMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"images" toKeyPath:#"images" withMapping:imageMapping]];
The following code is where I create the NSManagedObjects and call [RKObjectManager postObject:path:parameters:success:failure::
WTSRecord *record = [NSEntityDescription insertNewObjectForEntityForName:#"WTSRecord" inManagedObjectContext:self.managedObjectContext];
record.createBy = #"welcomed";
record.createDt = [NSDate date];
record.application = kWTSApplicationIdentify;
record.groupId = #"5";
WTSImage *image = [NSEntityDescription insertNewObjectForEntityForName:#"WTSImage" inManagedObjectContext:self.managedObjectContext];
image.height = [NSNumber numberWithFloat:mugshot.size.height];
image.width = [NSNumber numberWithFloat:mugshot.size.width];
image.imageData = UIImageJPEGRepresentation(imageData, 1.0);
image.type = kWTSCaptureTypeMugshot;
image.format = kWTSCaptureFormatJpeg;
image.status = kWTSCaptureStatusCaptured;
image.record = record;
[record addImagesObject:image];
RKObjectManager *manager = [RKObjectManager sharedManager];
[manager postObject:record path:#"records" parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error Sending Record" message:error.localizedDescription delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}];
When the success block is invoked, I check the sqlite database and there is 1 WTSRecord inserted and 2 WTSImages. One of the WTSImages has the correct FK to the WTSRecord and the PK from the database, while the other one appears to be orphaned (dbId and the FK to WTSRecord are not set).
Here is a link to the RestKit and Core Data trace logs: https://dl.dropboxusercontent.com/u/466390/restkit2.txt
Hopefully someone can help! Thanks!
EDIT After some more searching, I found this page: https://github.com/RestKit/RestKit/issues/1228
Do I have to create identification elements on the client side using UUIDs before POSTing them to a REST service? Will RestKit not be able to map request objects back to objects that were already created in the object store without first setting an identification attribute on the client side?
For the posted object, RestKit understands how to update that item with the response data, but that doesn't apply to the relationship contents. Technically it could be coded, but it isn't currently.
If you need the object in the relationship after the mapping to be the same object that you create then you have an issue. If you don't mind it being a different object then the issue is just to delete the duplicate...
Duplicate deletion:
Fetch request blocks aren't used when processing a POST response so you'd need to fetch the dupe and delete it manually. I'm going to assume that any image with a nil relationship to a record is a dupe so it's a relatively simple fetch to execute.
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.
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.
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.
I have neхt response:
{
"orders": [
{
"date": "2013-11-18T13:00:39.000Z",
"state": "new",
"id": 11,
"order_item_ids": [
27,
28
]
}
],
"order_items": [
{
"count": 2,
"id": 27,
"item_id": 1,
"order_id": 11
},
{
"count": 1,
"id": 28,
"item_id": 2,
"order_id": 11
}
]
}
And next object I want to map it:
#interface Order : NSObject
#property (nonatomic, strong) NSString* state;
#property (nonatomic, strong) NSMutableArray* orderItems;
#property (nonatomic, strong) NSDate* visitDate;
#end
orderItems should be array of:
#interface OrderItem : NSObject
#property (nonatomic) NSUInteger ID;
#property (nonatomic) NSUInteger itemID;
#property (nonatomic) NSUInteger count;
#end
In case of RKEntityMapping I should use addConnectionForRelationship to map order_item_ids to orderItems array via order_items response. But what should I do to do to connect it in case of RKObjectMapping? Off course I can map both orders and orderItems with separate response descriptors and than parse it, but I want to make RestKit do it for me. Another idea is to use CoreData and RKEntityMapping but I'm not sure that I want in this case, it will be overkill in this case.
You can't have it done automatically with RKObjectMapping because you can't index into both arrays in any way. The entity mapping only works because you can use foreign key mapping after both mappings have been performed. You can duplicate this by using multiple response descriptors and mappings and then combining the contents of the mapping result.