IOS - RESTKIT - mapping result in multiple ways - iphone

I am new to ios/RESTKIT. I am trying to consume a webservice from an ios device using RESTKIT. The json return could have 2 possible outcomes.
A) On Failure, json result looks like this (result is a string "null". Error code available):
{
status: false,
result: null
error: NO_SUCH_USER
}
Mapping for (A)
RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[WsReturn class]];
[mapping mapKeyPath:#"result" toAttribute:#"result"];
[mapping mapKeyPath:#"status" toAttribute:#"status"];
[mapping mapKeyPath:#"error" toAttribute:#"error"];
[objectManager.mappingProvider setMapping:mapping forKeyPath:#"/"];
B) On Success, it looks like (result is a "complex object". Error code is null):
{
status: true,
result: {
name: "Some User",
tasks: [
{
name: "Some Task1",
taskId: 10
},
{
name: "Some Task2",
taskId: 20
}
]
},
error: null
}
Mapping for (B)
RKObjectMapping* taskMapping = [RKObjectMapping mappingForClass:[Task class]];
[taskMapping mapKeyPath:#"name" toAttribute:#"name"];
[taskMapping mapKeyPath:#"taskId" toAttribute:#"taskId"];
[objectManager.mappingProvider setMapping:taskMapping forKeyPath:#"tasks"];
RKObjectMapping* resultMapping = [RKObjectMapping mappingForClass:[Result class]];
[resultMapping mapKeyPath:#"name" toAttribute:#"name"];
[resultMapping mapRelationship:#"tasks" withMapping:taskMapping];
[objectManager.mappingProvider setMapping:resultMapping forKeyPath:#"result"];
RKObjectMapping* cmplxMapng = [RKObjectMapping mappingForClass:[WsReturn class]];
[cmplxMapng mapKeyPath:#"status" toAttribute:#"status"];
[cmplxMapng mapKeyPath:#"error" toAttribute:#"error"];
[cmplxMapng mapRelationship:#"result" withMapping:resultMapping];
[objectManager.mappingProvider setMapping:cmplxMapng forKeyPath:#"/"];
Questions
1) (A) works alright. (B) does not. Can you provide some pointers?
2) For the same webservice call, "result" part could be a string (null) or a complex object. So how do I handle this in code? Which mapping do I pass? mapping or cmplxMapng (name changed to avoid horozontal scroll)?
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/myUrl"
objectMapping:HOW_TO_DECIDE_WHICH_MAPPING_TO_PASS_HERE delegate:self];
I believe this is a common scenario. I searched, but couldn't find related examples. Maybe I looked in the wrong places. Thoughts/pointers on how to approach this will help. Thanks.

So this is how I solved it -
1) I replaced this line:
[objectManager.mappingProvider setMapping:taskMapping forKeyPath:#"tasks"];
with this: (notice forKeyPath)
[objectManager.mappingProvider setMapping:uooMapping forKeyPath:#"result.tasks"];
2) Followed this thread http://groups.google.com/group/restkit/browse_thread/thread/89b25b3f0f7e0177 & added this line as suggested by Blake:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];
just before call to loadObjectsAtResourcePath
3) As for mapping the objects in different ways, I found that passing cmplxMapng always seems to work.
[[RKObjectManager sharedManager] loadObjectsAtResourcePath:#"/myUrl"
objectMapping:cmplxMapng delegate:self];
So if the "result" part comes back as the string "null", I get "(null)" for result & "status" & "error" fields are mapped correctly. Works!
Hope this helps someone.

Related

restkit: mapping to_many relations with core data

I'm creating my first RestKit application. I'm trying to map to_many relation to core data.
Speakers JSON
{
id: 1,
session_ids: [1,87]
},
{
id: 2,
session_ids: [2,88]
},
Mapping
sessionEntitiyMapping = [RKEntityMapping mappingForEntityForName:#"Session" inManagedObjectStore:self.managedObjectStore];
[sessionEntitiyMapping addAttributeMappingsFromDictionary:#{
#"id": #"session_id",
}];
sessionEntitiyMapping.identificationAttributes = #[ #"session_id" ];
speakerEntityMapping = [RKEntityMapping mappingForEntityForName:#"Speaker" inManagedObjectStore:self.managedObjectStore];
[speakerEntityMapping addAttributeMappingsFromDictionary:#{
#"name": #"name",
#"id": #"speaker_id",
}];
[speakerEntityMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"session_ids"
toKeyPath:#"sessions"
withMapping:sessionEntitiyMapping]];
speakerEntityMapping.identificationAttributes = #[ #"speaker_id" ];
Error
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFNumber 0x8569690> valueForUndefinedKey:]: this class is not key value coding-compliant for the key id.'
I think the problem is that you are using a primitive in your model, i.e session_id has NSInteger type. Unfortunately you are disallowed to use it, please use NSNumber instead.

RestKit object mapping failing in one project but not in Twitter example

Learning RESTKit.
Step 1: I have successfully run the Twitter example project.
Step 2: I have successfully modified the Twitter project to point to my own REST webservice... REST is able to interpret the results successfully, and map them to a custom object I built.
Step 3: I have created my own project space and copy-pasted the working code from Step 2 into this project with some minor edits, and strangely enough, RESTKit is failing to properly map the results. I am going crazy at this point.
Turning on the handy debugging traces included in RESTKit, both projects get to here:
2012-08-12 21:06:14.145 RKTwitter[9087:13003] D restkit.object_mapping:RKObjectMapper.m:320 Performing object mapping sourceObject
But Step2 Project gets this as the next message:
2012-08-12 21:06:14.294 RKTwitter[9087:13003] T restkit.object_mapping:RKObjectMapper.m:278 Examining keyPath '' for mappable content...
2012-08-12 21:06:14.301 RKTwitter[9087:13003] D restkit.object_mapping:RKObjectMapper.m:261 Found mappable collection at keyPath ''...
whereas my Step3 project gives up and dies:
2012-08-12 21:10:40.912 DogPark[9127:13203] D restkit.object_mapping:RKObjectMapper.m:351 The following operations are in the queue: ()
Rolled up my sleeves, and I have determined the precise point where the difference in code execution occurs within RESTKit:
In RKObjectMappingProvider.m, line 160 is the following method:
- (id)valueForContext:(RKObjectMappingProviderContext)context {
NSNumber *contextNumber = [NSNumber numberWithInteger:context];
return [mappingContexts objectForKey:contextNumber];
}
Which for Step2 returns a dictionary with 1 element in it, whereas my Step3 project returns a dictionary with 0 elements in it. This dictionary is used in RKObjectMapper.m, at line 332, where the foundMappable is either true for Step2 project or false for my Step3.
if ([mappingsForContext isKindOfClass:[NSDictionary class]]) {
results = [self performKeyPathMappingUsingMappingDictionary:mappingsForContext];
foundMappable = (results != nil);
Here is the code that seems correct but doesn't seem to want to run properly:
// init the Object Manager
RKURL *baseURL = [RKURL URLWithBaseURLString:SERVER_ADDRESS];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
// Mapping
RKObjectMapping *ownerMapping = [RKObjectMapping mappingForClass: [DogOwner class]];
[ownerMapping mapKeyPath:#"DogOwnerId" toAttribute:#"dogOwnerID"];
[ownerMapping mapKeyPath:#"DogName" toAttribute:#"dogName"];
[ownerMapping mapKeyPath:#"OwnerFirstName" toAttribute:#"ownerFirstName"];
[[RKObjectManager sharedManager].mappingProvider setObjectMapping:ownerMapping forResourcePathPattern:#"/DogOwner"];
[[RKObjectManager sharedManager] loadObjectsAtResourcePath: #"/dogowner" delegate:self];
Any ideas?
Thank you for the very comical narration to your problem.
One mistake was specifying a mapping to /Dogowner and trying to load /dogowner. Your comment above also gives two other huge problems
xml and json are not interchangable
your source keypath was wrong.

Why does SBJson JSON parsing only get the last key of interest?

I am using the following JSON: http://www.kb.dk/tekst/mobil/aabningstider_en.json
When I try to parse it by the key "location" as such:
// get response in the form of a utf-8 encoded json string
NSString *jsonString = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
// get most parent node from json string
NSDictionary *json = [jsonString JSONValue];
// get key-path from jason up to the point of json object
NSDictionary *locations = [json objectForKey:#"location"];
NSLog( #"%#", locations );
// iterate through all of the location objects in the json
for (NSDictionary *loc in locations )
{
// pull library name from the json object
NSString *name = [loc valueForKey:#"name"];
// add library data table arrays respectively
[ libraryNames addObject: ( ( name == nil | name.length > 0 ) ? name : #"UnNamed" ) ];
}
When I print the the object locations via NSLog:
{
address = "Universitetsparken 4, 3. etage, 2100 K\U00f8benhavn \U00d8";
desc = "";
lastUpdated = "";
latlng = "55.703124,12.559596";
link = "http://www.farma.ku.dk/index.php?id=3742";
name = "Faculty of Pharmaceutical Sciences Library";
parts = {
part = {
hour = {
day = "5.June Constitution Day (Denmark)";
open = Closed;
};
hours = {
hour = {
day = Friday;
open = "10-16";
};
};
name = main;
};
};
}
Which is only the last value for the "location" keys. Am I doing something wrong?
I tried validating the JSON via http://jsonlint.com/, however when I'd put in the JSON URL as above, it said "valid" - still only the last "locations" key was shown", however if I copy-paste it, it will not validate the JSON, and has to be fixed by removing new-lines from the string.
Also, when i try to parse the JSON and get the "name" fields, I get the following exception:
2012-05-08 15:37:04.941 iPhone App Tabbed[563:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x68bfe70> valueForUndefinedKey:]: this class is not key value coding-compliant for the key name.'
*** First throw call stack:
(0x13dc052 0x156dd0a 0x13dbf11 0x9d2f0e 0x941841 0x940ca9 0x4593 0xf964e 0x114b89 0x1149bd 0x112f8a 0x112e2f 0x1148f4 0x13ddec9 0x365c2 0x3655a 0x25b569 0x13ddec9 0x365c2 0x3655a 0xdbb76 0xdc03f 0xdbbab 0x25dd1f 0x13ddec9 0x365c2 0x3655a 0xdbb76 0xdc03f 0xdb2fe 0x5ba30 0x5bc56 0x42384 0x35aa9 0x12c6fa9 0x13b01c5 0x1315022 0x131390a 0x1312db4 0x1312ccb 0x12c5879 0x12c593e 0x33a9b 0x281d 0x2785)
terminate called throwing an exception(lldb)
It would make more sense if the "locations" tag was an array object enclosed by square brackets ([]), however right now it's only an sequence of normal key-value pairs... Sadly, that's the JSON I have to work with.
Please help and thanks a great deal! :)
Sincerely,
Piotr.
The JSON you've got to work with may be valid, but it doesn't make much sense. It has one big dictionary with the location key repeated many times. Most JSON parser will simply return the last value for the repeated key. It would be best if you could change the structure to use an array instead, but if you cannot there's still hope. You can read the stream and stuff the values from the location keys into an array as they come out of it. This is how you'd do that:
#interface BadJsonHelper : NSObject
#property(strong) NSMutableArray *accumulator;
#end
#implementation BadJsonHelper
- (void)parser:(SBJsonStreamParser *)parser foundArray:(NSArray *)array {
// void
}
- (void)parser:(SBJsonStreamParser *)parser foundObject:(NSDictionary *)dict {
[accumulator addObject:dict];
}
#end
You can drop that little helper class at the top of your file, outside the #implementation section of the class where you're doing your work. (There's no need for the #interface and #implementation being in different files.)
In your code, you would use it like this:
BadJsonHelper *helper = [[BadJsonHelper alloc] init];
helper.accumulator = [NSMutableArray array];
SBJsonStreamParserAdapter *adapter = [[SBJsonStreamParserAdapter new] init];
adapter.delegate = helper;
adapter.levelsToSkip = 1;
SBJsonStreamParser *parser = [[SBJsonStreamParser alloc] init];
parser.delegate = adapter;
switch ([parser parse: responseData]) {
case SBJsonStreamParserComplete:
NSLog(#"%#", helper.accumulator);
break;
case SBJsonStreamParserWaitingForData:
NSLog(#"Didn't get all the JSON yet...");
break;
case SBJsonStreamParserError:
NSLog(#"Error: %#", parser.error);
break;
}
This example was originally adapted from the following test:
https://github.com/stig/json-framework/blob/master/Tests/StreamParserIntegrationTest.m
Update: I created a fully functional example project that loads the JSON asynchronously and parses it. This is available from github.
The JSON is valid, however there is a basic problem regarding the definition of the array of items.
Instead of defining an array of locations using brackets, the JSON redefines the same location key/value pair over and over again. In other words JSON initially says the value of location is the collection with name "The Black Diamond", but immediately after it redefines it with the collection with name "Faculty Library of Humanities" and so on till the last location Faculty of Pharmaceutical Sciences Library".
The same is true for parts and hours.
If you can't fix the result of the JSON and you really need to get it working you may want to modify the JSON removing the "location" keys and adding brackets properly.
Edit
Alternatively you may use an NSScanner and process the JSON result manually. Kinda hacky but it will work as long as the JSON format doesn't change significantly.
Edit
This snipped of code should do the work...
NSString *jsonString = [[[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
int indx = 1;
for (;;)
{
NSRange locationRange = [jsonString rangeOfString:#"\"location\":"];
if (locationRange.location == NSNotFound) break;
jsonString = [jsonString stringByReplacingCharactersInRange:locationRange
withString:[NSString stringWithFormat:#"\"location%d\":", indx++]];
}
NSDictionary *locations = [json objectForKey:#"location"];
As you can see, the result of JSON parsing by SBJson is a NSDictionary. A dictionary contains key/value pairs, and the keys are unique identifiers for the pairs.
The JSON data you need to handle is valid but not a good one. Per RFC 4627 - 2.2:
An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members). A name is a string. A single colon comes after each name, separating the name from the value. A single comma separates a value from a following name. The names within an object SHOULD be unique.
Things like jQuery can parse the JSON also, but the result is the same as SBJson (the last one as the one). See Do JSON keys need to be unique?.
It is not a MUST, but it's still not a good practice. It would be much easier if you are able to change the structure of the JSON data on the server side (or even on the client side after receiving it) rather than parsing it as is.

Is this NSDictionary valid the way it is set up? Using XML to NSDictionary plugin

I'm using the XMLReader plugin found here https://github.com/Insert-Witty-Name/XML-to-NSDictionary to convert my XML data into an NS-Dictionary, but I am confused by how the dictionary is being set up. Here is what I am given:
{
response = {
"#status" = ok;
authentication = {
"#description" = "The username you provided is valid.";
"#login" = USERNAME;
"#response" = success;
"#user_id" = USERID;
};
};
}
I am trying to take the response object and say if key is equal to success then do something, but I'm not sure if this dictionary is even set up correctly.
Despite the weirdness of the #names as keys, it seems valid.
You could easily ask for your element response ( [ objectForKey:#"response"] ), get the [ objectForKey:#"authentication"] and then [ objectForKey:#"#response"] to check [yourString isEqualToString:#"success"].
Edit: Adding example
I've recently noticed some ; where commas should have been, but let's hope that's just a typo or something like it.
In case you have a doubt, you can always NSLog(#"%#", [yourFirstDictionary allKeys]); to make sure those are valid keys.
Let's call your first object myDict for the sake of the example.
NSDictionary * response = [myDict objectForKey:#"response"]; // this should have #status and authentication as keys
NSDictionary * authentication = [response objectForKey:#"authentication"];
NSString * innerResponse = [authentication objectForKey:#"#response"];
if ([innerResponse isEqualToString:#"success"]) {
// your code
}

iPhone: variable type returned by yajl

I'm quite new to iphone programming and I want to do the following stuff:
get data from a JSON REST web server
parse the received data using YAJL
Draw a graph with those data using core-plot
So, 1th item is fine, I use ASIHttpRequest which runs as espected
3rd is almost fine (I still have to learn how to tune core-plot).
The problem I have is regarding 2nd item.
I use YAJL as it seems to be the faster parser, so why not give it a try :)
Here is the part of code that gets the data from the server and parse them:
// Get server data
response_data = [request responseData];
// Parse JSON received
self.arrayFromData = [response_data yajl_JSON];
NSLog(#"Array from data: %#", self.arrayFromData);
The parsing works quite well in fact, the NSLog output is something like:
2010-06-14 17:56:35.375 TEST_APP[3733:207] Array from data :
{
data = (
{
val = 1317;
date = "2010-06-10T15:50:01+02:00";
},
{
val = 1573;
date = "2010-06-10T16:20:01+02:00";
},
........
{
val = 840;
date = "2010-06-11T14:50:01+02:00";
},
{
val = 1265;
date = "2010-06-11T15:20:01+02:00";
}
);
from = "2010-06-10T15:50:01+02:00";
to = "2010-06-11T15:20:01+02:00";
max = "2590";
}
According to th yajl-objc explanations http://github.com/gabriel/yajl-objc, the parsing returns a NSArray...
The thing is... I do not know how to get all the values from it as for me it looks more like a NSDictionary than a NSArray...
Could you please help ?
Thanks a lot,
Luc
edit1: it happens that this object is actually a NSCFDictionary (!), I am still not able to get value from it, when I try the objectFromKey method (that should work on a Dictionary, no ?) it fails.
It's returning an NSDictionary. NSCFDictionary is a private subclass and is immaterial to this discussion. So it looks like you'd retrieve stuff like:
NSDictionary * responseDictionary = ...;
NSArray * dataArray = [responseDictionary objectForKey:#"data"];
for (NSDictionary * dataPair in dataArray) {
NSLog(#"val: %#, date: %#", [dataPair objectForKey:#"val"], [dataPair objectForKey:#"date"]);
}