restkit mapping and validate response - ios

It's the first time I use restkit and I am interested in the automatic mapping (and maybe later also in the integration with CoreData).
Currently I managed to perform a GET request and map the response to a simple object with the following code:
RKObjectMapping *myMapping = [[self class] objectMappingForClass:[MyClass class]];
[myMapping mapKeyPath:#"Name" toAttribute:#"name"];
[myMapping mapKeyPath:#"Value" toAttribute:#"value"];
....
+ (RKObjectMapping*)objectMappingForClass: (Class)class
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:class];
mapping.rootKeyPath = #"Data";
mapping.performKeyValueValidation = NO;
// mapping.ignoreUnknownKeyPaths = YES;
mapping.setNilForMissingRelationships = YES;
mapping.setDefaultValueForMissingAttributes = YES;
return mapping;
}
I retrieve the data from the server in this way:
RKObjectMapping *defaultPropertiesMapping = [self.objectManager.mappingProvider objectMappingForClass:[MyClass class]];
RKObjectLoader *request = [self.objectManager loaderWithResourcePath:GetDefaultPropertiesURL];
request.objectMapping = defaultPropertiesMapping;
RKResponse *response = [request sendSynchronously];
The problem is that for the response the server does not use the HTTP status, but an attribute in the JSON payload (the payload is something like:
{ "Status" : "OK", "Data" : { //real payload } }
How can I check that Status attribute??

If you set the object loader delegate to the class you are sending the request from, you can implement the following method:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout __autoreleasing id *)mappableData; {
if([*mappableData valueForKey:#"Status"] isEqualToString:#"OK"){
// Do something
}
}
You can then check the value of the status and handle the response accordingly. This method is called before the object is mapped to Core Data. Also, make sure your class adopts the RKObjectLoaderDelegate protocol or this won't work.
If you ever want to remove data from the the response, use the following method:
[*mappableData removeObjectForKey:#"Data"];
Any keys removed from *mappableData won't make it to the object mapper.

Related

iOS RestKit POST unmodified JSON string

I have a special need to send a pre-formatted JSON string to the server. Due to the server using older Microsoft technology the JSON elements MUST be in a certain order. If I use the standard JSON processing of RestKit the JSON elements come from a dictionary and are added in hash order. Sadly this will not work for this one special case.
How can I send a pre-formatted JSON string instead of an NSDictionary that is converted to a JSON string with RestKit 0.2x?
Here is the code for the request using NSDictionary
RKObjectManager *objectManager = self.createObjectManager;
RKObjectMapping *requestMapping = [EssenceRequest.objectMapping inverseMapping];
[objectManager addRequestDescriptor:[RKRequestDescriptor requestDescriptorWithMapping:requestMapping
objectClass:EssenceRequest.class
rootKeyPath:nil
method:RKRequestMethodPOST]];
RKObjectMapping *responseMapping = EssenceRoot.objectMapping;
RKResponseDescriptor* essenceResponse = [RKResponseDescriptor responseDescriptorWithMapping:responseMapping
method:RKRequestMethodPOST
pathPattern:nil
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[objectManager addResponseDescriptor:essenceResponse];
EssenceRequest *dataObject = [[EssenceRequest alloc] initWithContextAndHandle:uniqueHandle essenceHandle:essenceHandle];
[objectManager postObject:dataObject
path:[NSString stringWithFormat:#"%#%#%#GetEssences", Connection.apiPrefix, Connection.svcMedia, Connection.jsonSecure]
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[serverResponseDelegate serverResponseSuccess:operation mappingResult:mappingResult ];
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
[serverResponseDelegate serverResponseFailure:operation error:error];
}];
The EssenceRequest
- (id)initWithContextAndHandle:(NSString *)uniqueHandle essenceHandle:(NSString *)essenceUH;
{
self = [super init];
if (self != nil) {
_request = #{
#"__type" : #"SpecificEssenceLocationRequest:#Messaging.Media",
#"Action" : #"1",
#"ContextUH" : uniqueHandle,
#"EssenceUH" : essenceUH
};
}
return self;
}
+ (RKObjectMapping*)objectMapping
{
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:EssenceRequest.class];
[mapping addAttributeMappingsFromDictionary:#{
#"request": #"request"
}];
return mapping;
}
The "__type" item must be the first time in the JSON request body. Right now with it being in a dictionary it shows up later in the body when the dictionary is converted to a JSON string.
I know this is poor JSON handling on the server. They will fix it at some point and not require the __type any more but for now I need to send it as needed. I was able to do this in my Android code so I know the request will work once I have the NSString formatted.
Disclaimer: following answer is just my own opinion / suggestion.
Use +[RKMimeTypeSerialization unregisterClass:[RKNSJSONSerialization class]] to unregister default RestKit json serialization class. Then write your own class with "hacked" keys order. Register it through +[RKMimeTypeSerialization registerClass:[RKMYJSONSerialization class] forMINEType:RKMIMETypeJSON]
This way your won't change any API's - just "inject" your code into serialization/deserialization mechanism (and this is what you actually need).
The default implementation of RKNSJSONSerialization is quite simple:
+ (id)objectFromData:(NSData *)data error:(NSError **)error
{
return [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
}
+ (NSData *)dataFromObject:(id)object error:(NSError **)error
{
return [NSJSONSerialization dataWithJSONObject:object options:0 error:error];
}
I think, you can go further yourself and write your own, based, of course on NSJSONSerialization or some another JSON serialization mechanism.
Taking Petro's answer a step further. This solution will maintain the functionality of all other requests.
After implementation you can wrap any JSON string in a SPRawJSON to send it as raw JSON for any request.
SPJSONSerialization.h
#import <RestKit/RestKit.h>
#interface SPRawJSON : NSObject
#property (nonatomic, readonly) NSString *json;
-(instancetype)initWithJSON:(NSString*)json;
+(RKObjectMapping*)mapping;
#end
#interface SPJSONSerialization : NSObject <RKSerialization>
#end
SPJSONSerialization.m
#import "SPJSONSerialization.h"
#implementation SPRawJSON
-(instancetype)initWithJSON:(NSString*)json
{
self = [super init];
if (self) {
_json = json;
}
return self;
}
+(RKObjectMapping*)mapping {
RKObjectMapping *mapping = [RKObjectMapping mappingForClass:[SPRawJSON class]];
[mapping addAttributeMappingsFromDictionary:#{ #"rawJSON": #"self" }];
return mapping;
}
#end
#implementation SPJSONSerialization
+ (id)objectFromData:(NSData *)data error:(NSError **)error {
return [RKNSJSONSerialization objectFromData:data error:error];
}
+ (NSData *)dataFromObject:(id)object error:(NSError **)error {
if ([object isKindOfClass:NSDictionary.class]) {
NSDictionary *dict = object;
id rawJSONObj = dict[#"rawJSON"];
if (rawJSONObj && [rawJSONObj isKindOfClass:SPRawJSON.class]) {
return [[(SPRawJSON*)rawJSONObj json] dataUsingEncoding:NSUTF8StringEncoding];
}
}
return [RKNSJSONSerialization dataFromObject:object error:error];
}
#end
Registering the mapping
RKObjectManager *objectManager = [RKObjectManager sharedManager];
// Make requests bodies be sent as JSON
objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
// Add inverse mapping for the request
RKRequestDescriptor *descriptor = [RKRequestDescriptor requestDescriptorWithMapping:[SPRawJSON mapping].inverseMapping objectClass:[SPRawJSON class] rootKeyPath:nil method:RKRequestMethodPOST];
[objectManager addRequestDescriptor:descriptor];
Registering the JSON Serializer
// Replace standard JSON Serializer with our custom one that accepts raw json strings as well (SPRawJSON)
let currentJSONSerializer = RKMIMETypeSerialization.serializationClass(forMIMEType: RKMIMETypeJSON)
RKMIMETypeSerialization.unregisterClass(currentJSONSerializer)
RKMIMETypeSerialization.registerClass(SPJSONSerialization.self, forMIMEType: RKMIMETypeJSON)
Example code for sending request
NSString *myJSON = #"{\"exampleKey\": \"Example value\"}";
SPRawJSON *rawJSON = [[SPRawJSON alloc] initWithJSON:myJSON];
RKObjectManager *objectManager = [RKObjectManager sharedManager];
[objectManager postObject:rawJSON path:#"foo/bar" parameters:nil success: ... failure: ...]
Notice that the mapping only maps POST requests, so if you want it to work for PUT, etc, you need to map that as well.

RestKit 0.10.0 with foursquare API's not retrieving the response when using blocks

Um beginner with RestKit, first example for me was on foursquare API's and I've used RestKit with Blocks not delegates.
I want to retrive the name's of venues,this is the JSON response
and this is my code:
// App Delegate.m
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURLString:#"https://api.Foursquare.com/v2"];
RKManagedObjectStore *objectStore = [RKManagedObjectStore objectStoreWithStoreFilename:#"Venue.sqlite"];
objectManager.objectStore = objectStore;
objectManager.serializationMIMEType = RKMIMETypeJSON;
RKManagedObjectMapping *venueMapping = [RKManagedObjectMapping mappingForClass:[Venue class] inManagedObjectStore:objectStore];
[venueMapping mapKeyPath:#"id" toAttribute:#"id"];
[venueMapping mapKeyPath:#"name" toAttribute:#"name"];
venueMapping.primaryKeyAttribute = #"id";
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
then in myViewController.m
-(void)loadVenues{
// When caling loadObjectsAtResourcePath method it specify RKObjectLoader which is the actual request.
// within these block you can take more options to controll the request.
[[RKObjectManager sharedManager]loadObjectsAtResourcePath:#"/venues/40a55d80f964a52020f31ee3?oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP&v=20130512" usingBlock:^(RKObjectLoader* loader)
{
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[Venue class]];
loader.onDidLoadObject = ^(NSArray *objects)
{
NSLog(#"onDidLoadObject Blocks");
self.data = objects;
[self.tableView reloadData];
};
}
];
}
and the app is entering the block of onDidLoadObject but every time the array is empty !!
even when I test the link on browser it comes with data.
When I debug the loader.URL it always come with these
https://api.Foursquare.com/v2/venues/40a55d80f964a52020f31ee3?v=20130512&oauth_token=FNQPN5P5EKLJ5IQ44TMWO00I3W033M0Y1TKINW2OTF2VIOTP -- https://api.Foursquare.com/v2 -- https://api.Foursquare.com/v2
I don't know why load.URL is wrong ?!
I think um calling the 4square API's with the wrong way, anyone can help ? :)
-Put Your mapping as a class method to be accessed from all application classes, and done only once.
-change the Attribute "id" as it is reserved in Objective-C.
-add this to the block
[loader setObjectMapping:[RestKitMapping yourMapping]];
-then add this with your mapping code
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venue"];
-And use Delegates instead of blocks
I've two concerns regarding the above code:
1- Apparently the above JSON response, lists just one Venue .. So KeyPath should be "response.venue" not "response.venues"
2- Where's the mapping for ID? .. which is the primary key that RestKit uses to insert into DB? You need to set the primary key mapping.

Consuming web service with RESTKIT

I am a beginner to RESTKIT and have only just recently tested it out on foursquare public api from ray's tutorial http://www.raywenderlich.com/13097/intro-to-restkit-tutorial.
Although i get the gist of it, there are some part which i do not understand, and would like pointer for it, so that i can consume my own web service.
- (void)viewDidLoad
{
[super viewDidLoad];
RKURL *baseURL = [RKURL URLWithBaseURLString:#"https://api.Foursquare.com/v2"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
RKObjectMapping *venueMapping = [RKObjectMapping mappingForClass:[Venue class]];
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venues"];
[self sendRequest];
}
how do I change
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"response.venues"];
to accommodate my own webMethod? (my webMethod is shown below)
Currently, I upload my file to IIS for testing purpose, and am using the IP for web service. (I am constantly changing work area, so I designate it as myIPAddress for easier communication)
-- My service code (changed EDIT:now return JSON)
[WebMethod]
[ScriptMethod( ResponseFormat = ResponseFormat.Json)]
public void testTextJSON()
{
string text = "Testing for Json!";
List<string> arrayList = new List<string>();
arrayList.Add(text);
JavaScriptSerializer js = new JavaScriptSerializer();
string name = js.Serialize(arrayList);
Context.Response.Write(name);
}
return - ["Testing for Json!"]
EDIT- what I changed currently for viewDidLoad and sendRequest to test for my own service
- (void)viewDidLoad
{
[super viewDidLoad];
RKURL *baseURL = [RKURL URLWithBaseURLString:#"http://192.168.1.12"];
RKObjectManager *objectManager = [RKObjectManager objectManagerWithBaseURL:baseURL];
objectManager.client.baseURL = baseURL;
RKObjectMapping *venueMapping = [RKObjectMapping mappingForClass:[venue class]];
[venueMapping mapKeyPathsToAttributes:#"name", #"name", nil];
[self sendRequest];
}
and
- (void)sendRequest
{
RKObjectManager *objectManager = [RKObjectManager sharedManager];
RKURL *URL = [RKURL URLWithBaseURL:[objectManager baseURL] resourcePath:#"/webService/webService1.asmx/testTextJSON"];
objectManager.acceptMIMEType = RKMIMETypeJSON;
objectManager.serializationMIMEType = RKMIMETypeJSON;
[[RKParserRegistry sharedRegistry] setParserClass:[RKJSONParserJSONKit class] forMIMEType:#"text/plain"];
[objectManager loadObjectsAtResourcePath:[NSString stringWithFormat:#"%#", [URL resourcePath]] delegate:self];
}
EDIT n+1 - here are some of my error message, maybe someone can tell me what went wrong?
2012-11-25 06:49:20.925 fourSquareAPI[352:12e03] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSCFString 0x8354430> valueForUndefinedKey:]: this class is not key value coding-compliant for the key venues.'
If I remove [objectManager.mappingProvider setMapping:venueMapping forKeyPath:#"venues"];
I would get
2012-11-25 06:52:47.495 fourSquareAPI[368:11603] response code: 200
2012-11-25 06:52:47.499 fourSquareAPI[368:12e03] W restkit.object_mapping:RKObjectMapper.m:87 Adding mapping error: Could not find an object mapping for keyPath: ''
2012-11-25 06:52:47.499 fourSquareAPI[368:12e03] E restkit.network:RKObjectLoader.m:231 Encountered errors during mapping: Could not find an object mapping for keyPath: ''
2012-11-25 06:52:47.502 fourSquareAPI[368:11603] Error: Could not find an object mapping for keyPath: ''
Can someone please teach me what to do?? Any help would be greatly appreciated, I would really like to learn how to use RESTKIT to consume a webservice.
The key path response.venues is a path to the attribute with respect to the server response that is in JSON or XML format. In this case, the server returns "response" which has the key "venues". This is most likely a list of venues that you would apply the mapping to. You have to adapt that based on your server response.
If you want to handle a specific value before the mapping, use this function:
- (void)objectLoader:(RKObjectLoader *)loader willMapData:(inout __autoreleasing id *)mappableData; {
if([*mappableData valueForKey:#"result"] != nil){
[*mappableData removeObjectForKey:#"result"]; //This key won't be mapped now
}
}

Restkit sending Get request instead of Post

I'm just starting to use Restkit and am trying to send a regular POST request "serverUrl/account/authenticate.xml?OPTIONS" to get a response as XML. This is the code I use to call the URL:
-(void) function{
// OBJECT MANAGER
RKObjectManager* manager = [RKObjectManager objectManagerWithBaseURL:serverUrl];
[manager.router routeClass:[CBUser class] toResourcePath:#"/account/authenticate" forMethod:RKRequestMethodPOST];
// OBJECT MAPPING
RKObjectMapping* mapping = [RKObjectMapping mappingForClass:[CBUser class]];
mapping = [manager.mappingProvider objectMappingForKeyPath:#"user"];
[mapping mapKeyPath:#"id" toAttribute:#"userId"];
[manager loadObjectsAtResourcePath:str objectMapping:mapping delegate:self];
}
// DELEGATE
- (void)objectLoader:(RKObjectLoader*)objectLoader didLoadObjects:(NSArray*)objects {
CBUser* user = [objects objectAtIndex:0];
NSLog(#"Loaded Contact ID #%# -> Firstname: %#, Lastname: %#", user.userId, user.firstname, user.lastname);
}
- (void)objectLoader:(RKObjectLoader*)objectLoader didFailWithError:(NSError*)error {
NSLog(#"Encountered an error: %#", error);
}
Looking at the server log it seems it receives a GET request instead of a POST even though I'm using the "RKRequestMethodPOST" option.
Here is my error form my logs:
Started GET "/account/authenticate.xml?commit=Login&authenticity_token=iPhone&user%5Bpassword=XXXXX&user%5Bemail=XXXXXXX%5D&user%5Bdevice_token=XXXXXXX" for 192.168.106.30 at 2012-01-25 19:20:03 -0800
AbstractController::ActionNotFound (The action 'show' could not be found for AccountController):
Rendered /Users/Guillaume/.rvm/gems/ruby-1.9.2-p290/gems/actionpack-3.0.9/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb within rescues/layout (1.2ms)
What am I missing?
Hopefully this code snippet of my app will help. You setup your mappings, say in your delegate. Note the "forMethod"
RKObjectRouter *router;
RKObjectMapping* createAccountSerializationMapping = [RKObjectMapping mappingForClass:[Cr eateAccount class]];
[createAccountSerializationMapping mapAttributes:#"email", #"pwd", #"uname", nil];
[[RKObjectManager sharedManager].mappingProvider setSerializationMapping:createAccountSerializationMapping forClass:[CreateAccount class]];
router = [RKObjectRouter new] ;
[router routeClass:[CreateAccount class] toResourcePath:#"/registration/rest/users/create_account" forMethod:RKRequestMethodPOST];
[RKObjectManager sharedManager].router = router;
and then later, when you want to post an object
[[RKObjectManager sharedManager] postObject:user delegate:self];

How do I have two post routes for the same class in RestKit

Since I couldn't figure out how to set up two different POST resource paths for the same class , I tried manually creating the RKObjectLoader request but it seems to keep sending a GET request instead of a POST even though I've set the method to POST. Here is my code
User *user = [[User alloc] init];
user.uname = uname;
user.pwd = pwd;
RKObjectManager *svc = [RKObjectManager sharedManager];
RKObjectMapping* mapping = [svc.mappingProvider objectMappingForClass:[User class]];
// what I was using before I needed two post resource paths//[svc postObject:user mapResponseWith:mapping delegate:self];
RKObjectLoader *loader = [svc loadObjectsAtResourcePath:authResourcePath objectMapping:mapping delegate:self];
[loader setMethod:RKRequestMethodPOST];
loader.userData = [NSNumber numberWithInt:RequestLogin];
loader.params = [NSDictionary dictionaryWithObjectsAndKeys:
uname, #"uname",
pwd, #"pwd",
nil];
[loader setSourceObject:user];
[loader send];
[user release];
In cases where you have more than one path to POST or PUT to, the easiest thing to do is use the block form of the postObject: invocation and specify the destination resourcePath yourself:
[[RKObjectManager sharedManager] postObject:foo delegate:bar block:^(RKObjectLoader *loader) {
loader.resourcePath = #"/my/destinationPath";
}];
We may introduce a named route concept at some point that would let you disambiguate the routes using names, but for now its purely based on the HTTP verb.
Note that you do NOT and cannot register the secondary path on the router -- you are sidestepping it completely for the secondary path.
In order to complete Blake Watters answer if the different route need different objectMapping you will need to do:
[[RKObjectManager sharedManager] postObject:query delegate:saveJobQueryHandler block:^(RKObjectLoader* loader) {
loader.objectMapping = NEW_MAPPING;
loader.resourcePath = #"/other/url";
loader.targetObject = nil; // Important
}];
For more information about loader.targetObject = nil; read sendObject:delegate:block:

Resources