how to i set response mapping to manager with path pattern ..if the getobjects at path is different from path pattern that is used to map the response.
[manager addResponseDescriptorsFromArray:
#[[RKResponseDescriptor responseDescriptorWithMapping:categoryMapping
pathPattern:A
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]];
[manager getObjectsAtPath:A/ID
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#" Category success");
[self.delegate didReceiveAssignedCategories];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Category failure");
}];
response mapping path ie:A must be set to dynamic path used to getobject ie:A/ID .
ex:
Call 1)
A = /getAllCategories
A/ID = /getAllCategories/123
call 2)
A = /getAllCategories
A/ID = /getAllCategories/456
response mapping is same for 123, 456
only while getting the objects i am using different urls ie: with id's attached.
how to do that ?
If you have 2 path patters which both return the same type of data then you can use the same mapping with 2 different response descriptors.
If you have 1 path pattern which can return 2 different types of data then you need to use RKDynamicMapping to 'intercept' the incoming data and decide which mapping is actually required.
From your edited question, 'pattern' is the important thing that you have misunderstood. You need to use a path pattern, not a static path:
#"getAllCategories/:identity"
1) First create response mapping like
[manager addResponseDescriptorsFromArray:
#[[RKResponseDescriptor responseDescriptorWithMapping:categoryMapping
pathPattern:#"getAllCategories/:categoryID"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]];
2)Create Class with categoryID in it.
[CategoryRequest class]
3) create object of that class and set categoryID
CategoryRequest *categoryRequest = [CategoryRequest alloc] init];
categoryRequest.categoryID = #"123";
4)call getobject using that object
[manager getObject:categoryRequest
path:#"getAllCategories/123"
parameters:params
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"Success");
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failure");
}];
if another call is required to be made for same mapping create object of of category request class set new category id and call get object using that categoryresquest and required path patter.
Related
Is there a way to differ between mapping results which are empty due to there being no data do map att the key path and a mapping result which is empty due do skipping objects through KVC.
The problem I have is that I am working with a API which is not very well thought through, and I need to find a way to know when I am at the end of the page. The issue is that there is a possibility that the mapping result can be empty due to all the objects being returned are "deleted".
TLDR; Is there a way to see the amount of mapping errors through mapping result on a completed request.
EDIT
I solved my own problem by reading the documentation again. The solution was to create a subclass of RKObjectRequestOperation and then register it.
[[RKObjectManager sharedManager] registerRequestOperationClass:[CustomRequestOperation class]];
This mean that I could implement the following which would listen to mapping errors and increment a counter and add it to mappingMetaData when the mapping completed.
- (void)setCompletionBlockWithSuccess:(void ( ^ ) ( RKObjectRequestOperation *operation , RKMappingResult *mappingResult ))success failure:(void (^) ( RKObjectRequestOperation *operation , NSError *error ))failure {
__block typeof(skipCount) temp = skipCount;
[super setCompletionBlockWithSuccess:^void(RKObjectRequestOperation *operation , RKMappingResult *mappingResult) {
if (success) {
operation.mappingMetadata = #{#"skipCount" : #(temp)};
success(operation, mappingResult);
}
} failure:^void(RKObjectRequestOperation *operation , NSError *error) {
if (failure) {
failure(operation, error);
}
}];
}
- (void)mapper:(RKMapperOperation *)mapper didFailMappingOperation:(RKMappingOperation *)mappingOperation forKeyPath:(NSString *)keyPath withError:(NSError *)error {
skipCount++;
}
- (void)mapperWillStartMapping:(RKMapperOperation *)mapper {
skipCount = 0;
}
I have two EndPoints:
http://rest.domain.com/invite
http://rest.domain.com/template
Depending on what options the User selects, I need to able to PUT, POST, & Delete on both EndPoints.
Mapping for all three methods is slightly different. Mapping for each EndPoint is also different.
What's the best way to setup mappings so they are loaded once and can be used multiple times for each EndPoint depending upon what option (PUT, POST, or Delete) the User selects? I have to accomplish this on one storyboard Scene!
Currently, below is the code that I use when POSTing to the /invite EndPoint (it crashes after the first POST b/c I'm remapping):
- (void)sendInvite:(NSInteger)methodType
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.objectManager = [self getObjectManager];
self.objectManager.managedObjectStore = appDelegate.managedObjectStore;
RKEntityMapping *invitationMapping = [RKEntityMapping mappingForEntityForName:kInvite
inManagedObjectStore:self.objectManager.managedObjectStore];
RKEntityMapping *activityMapping = [RKEntityMapping mappingForEntityForName:kActivity
inManagedObjectStore:self.objectManager.managedObjectStore];
Invite *invitation;
switch (methodType) {
case POST:
{
invitationMapping = [RESTMappingProvider invitionPostMapping:invitationMapping];
activityMapping = [RESTMappingProvider activityPostMapping:activityMapping];
[invitationMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:kActivitiesRelationship
toKeyPath:kActivitiesRelationship
withMapping:activityMapping]];
invitation = [self inviteForMethod:POST]; //This method just assigns values to the attributes
[self setupDescriptors:invitationMapping forKeyPath:kMeetupKeyPath descriptorClassIsTemplate:NO];
[self.objectManager.HTTPClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[self.objectManager.managedObjectStore.mainQueueManagedObjectContext saveToPersistentStore:NO];
[self.objectManager postObject:invitation path:kMeetupKeyPath parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self removeDuplicateObjects]; //After removing relationship Dups, I save to persistent store
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
}];
}
break;
case PUTACCEPT:
case PUTDECLINE:
case PUTEDIT:
{
invitationMapping = [RESTMappingProvider invitionPutMapping:invitationMapping];
activityMapping = [RESTMappingProvider activityPutMapping:activityMapping];
[invitationMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:kActivitiesRelationship
toKeyPath:kActivitiesRelationship
withMapping:activityMapping]];
invitation = [self inviteForMethod:methodType]; //This method just assigns values to the attributes
[self setupDescriptors:invitationMapping forKeyPath:kMeetupKeyPath descriptorClassIsTemplate:NO];
[self.objectManager.HTTPClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[self.objectManager putObject:invitation path:kMeetupKeyPath parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failure in PUT");
}];
}
break;
}
}
I do something similar for the Templates EndPoint
- (void)saveAsTemplate
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.objectManager = [self getObjectManager];
self.objectManager.managedObjectStore = appDelegate.managedObjectStore;
RKEntityMapping *invitationMapping = [RKEntityMapping mappingForEntityForName:kInviteTemplates
inManagedObjectStore:self.objectManager.managedObjectStore];
invitationMapping = [RESTMappingProvider invitionTemplateMapping:invitationMapping];
RKEntityMapping *activityMapping = [RKEntityMapping mappingForEntityForName:kActivityTemplates
inManagedObjectStore:self.objectManager.managedObjectStore];
activityMapping = [RESTMappingProvider activityTemplateMapping:activityMapping];
[invitationMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:kTemplateActivitiesRelationship
toKeyPath:kTemplateActivitiesRelationship
withMapping:activityMapping]];
STInvitesTemplate *invitation = [self templateForInvite];//this method assigns values to the attributes
[self setupDescriptors:invitationMapping forKeyPath:kTemplatesKeyPath descriptorClassIsTemplate:YES];
[self.objectManager.HTTPClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
[self.objectManager postObject:invitation path:kTemplatesKeyPath parameters:nil success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
}];
}
Request & Response Descriptor:
- (void)setupDescriptors:(RKEntityMapping *)invitationMapping forKeyPath:(NSString *)keyPath descriptorClassIsTemplate:(BOOL)isTemplate
{
RKRequestDescriptor *requestDescriptor;
if (isTemplate)
{
requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[invitationMapping inverseMapping] objectClass:[Template class] rootKeyPath:nil method:RKRequestMethodAny];
}
else
{
requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[invitationMapping inverseMapping] objectClass:[Invite class] rootKeyPath:nil method:RKRequestMethodAny];
}
NSIndexSet *statusCodeSet = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful);
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:invitationMapping
method:RKRequestMethodGET
pathPattern:keyPath
keyPath:nil
statusCodes:statusCodeSet];
self.objectManager.requestSerializationMIMEType = RKMIMETypeJSON;
[self.objectManager addRequestDescriptor:requestDescriptor];
[self.objectManager addResponseDescriptor:responseDescriptor];
}
I know the approach above is incorrect as Xcode crashes after I POST or PUT. I haven't implemented Delete yet b/c I'm not sure how to set this up correctly.
Do I load the mappings ONCE in viewDidLoad? Do I create PUT, POST, DELETE x 2 EndPoints = 6 RKEntityMappings?
Need some guidance on best practice. Code sample or some step-by-step instructions would be great!
You need to create an many different mappings as you have different structure responses to process. If the responses for one entity type are all subsets of some common superset then you can use just one (for mapping responses and another for requests). You don't say anything about the expected JSON so I can't tell.
In your code I see 2 request descriptors and 1 response descriptor. The requests match against any method so will always be used. The response descriptor matches only GET responses so won't work for anything. You should likely have one response descriptor per end point per method (as you say they need different mappings to be applied to each).
viewDidLoad isn't necessarily the correct place to configure this. It looks like you have a single object manager and this configuration code should run when it is created (not when it is used, brcause the view can be loaded multiple times and the configuration would be duplicated).
I am doing a simple GET request with AFNetworking
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"JSON: %#", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
Once I have made the request I want to be able to access the responseObject from any other method in the class.
I want to be able to save the responseObject so I can do something like display the output in a tableview.
It's common to creat object models that will be represented by JSON. When you get the response you would then parse the data into the models. The approach we use is to return the response to the requester through a completion block. You don't have to parse the JSON into strongly typed objects, but it really is helpful long term. It's probably a good idea to farm out the network request operations into a separate class (called a service) as well. This way you can instantiate a new service and get notified through a completion block that it is finished. For example your service's request signature could look like this:
typedef void(^HelloWorldCompletionHandler)(NSString *helloWorld, NSError *error);
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler;
// implementation
- (void)requestHelloWorldData:(HelloWorldCompletionHandler)completionHandler {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
id JSONResponse = [operation responseObject];
if (operation.error) {
completionHandler(nil, error);
} else {
// parse the response to something
id parserResult = [self parseJSONResponse:JSONResponse];
completionHandler(parserResult, nil);
}
}];
This way you'll know when the network request is complete, and you can set the data you want on a property within your class. Then you could call tableView.reloadData in order to use the data in your table.
All that code would go into a service type class. I like to organize my services by responsibility. I don't know how many different data calls you make, but we have several for our project. If for instance you were making a weather app you could potentially organize by Current Conditions, Daily Forecasts, and Hourly Forecasts. I would make a service for each one of these requests. Say I created a CurrentConditionsService. The header would look something like this:
typedef void(^CurrentConditionsCompletionHandler)(CurrentConditions *currentConditions, NSError *error);
#interface CurrentConditionsService : NSObject
// locationKey is some unique identifier for a city
+ (instancetype)serviceWithLocationKey:(NSString *)locationKey;
- (void)retrieveCurrentConditionsWithCompletionHandler:(CurrentConditionsCompletionHandler)completionHandler;
#end
Then in my implementation file I would make the request and invoke the given completion handler like I demonstrated above. This pattern can be followed by many different services to the point where all your services could inherit from a base class that handles the request/response portions. Then your subclasses could override specific methods and handle/parse the data appropriately based on type.
If you go the route of parsing the JSON responses into model objects, all your parsers will need to conform to a protocol. This way in your super class it doesn't matter what the concrete implementation of your parser is. You supply the super class with a concrete implementation and all it knows how to do is invoke the parser and return the response.
An example JSON parser protocol would look like this:
#protocol AWDataParser <NSObject>
#required
- (id)parseFromDictionary:(NSDictionary *)dictionary;
- (NSArray *)parseFromArray:(NSArray *)array;
#end
And invoking it in your services super class:
- (id)parseJSONResponse:(id)JSONResponse error:(NSError **)error {
NSAssert(self.expectedJSONResponseClass != nil, #"parseResponse: expectedJSONResponseClass cannot be nil");
NSAssert(self.parser != nil, #"parseResponse: parser cannot be nil");
id parserResult = nil;
if (![JSONResponse isKindOfClass:self.expectedJSONResponseClass]) {
//handle invalid JSON reponse object
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:#{#"Invalid JSON type": [NSString stringWithFormat:#"expected: %#, is: %#",self.expectedJSONResponseClass, [JSONResponse class]]}];
}
} else {
if (self.expectedJSONResponseClass == [NSArray class]) {
parserResult = [self.parser parseFromArray:JSONResponse];
}else {
parserResult = [self.parser parseFromDictionary:JSONResponse];
}
if (!parserResult) {
if (error) {
*error = [NSError errorWithDomain:NetworkServiceErrorDomain code:kNetworkServiceErrorParsingFailure userInfo:nil];
}
}
}
return parserResult;
}
Use this approach:
NSURL *COMBINAT = [[NSURL alloc] initWithString:#"http://someapi.com/hello.json"];
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:
COMBINAT];
[self performSelectorOnMainThread:#selector(savedata:) withObject:data waitUntilDone:YES];
});
then simply call:
- (void)savedata:(NSData *)responseData {
NSError* error;
NSLog(#"Answer from server %#", responseData);
// ... your code to use responseData
}
Just create a property:
#property(nonatomic, strong) id savedResponseObject;
and set it in the success handler of the request:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:#"http://someapi.com/hello.json"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
self.savedResponseObject = responseObject;
}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(#"Error: %#", error);
}];
Then you will be able to access it from other places in your class by referencing:
self.savedResponseObject
how to i set response mapping to manager with path pattern ..if the getobjects at path is different from path pattern that is used to map the response.
[manager addResponseDescriptorsFromArray:
#[[RKResponseDescriptor responseDescriptorWithMapping:categoryMapping
pathPattern:A
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]];
[manager getObjectsAtPath:A/ID
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#" Category success");
[self.delegate didReceiveAssignedCategories];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Category failure");
}];
response mapping path ie:A must be set to dynamic path used to getobject ie:A/ID .
ex:
Call 1)
A = /getAllCategories
A/ID = /getAllCategories/123
call 2)
A = /getAllCategories
A/ID = /getAllCategories/456
response mapping is same for 123, 456
only while getting the objects i am using different urls ie: with id's attached.
how to do that ?
If you have 2 path patters which both return the same type of data then you can use the same mapping with 2 different response descriptors.
If you have 1 path pattern which can return 2 different types of data then you need to use RKDynamicMapping to 'intercept' the incoming data and decide which mapping is actually required.
From your edited question, 'pattern' is the important thing that you have misunderstood. You need to use a path pattern, not a static path:
#"getAllCategories/:identity"
1) First create response mapping like
[manager addResponseDescriptorsFromArray:
#[[RKResponseDescriptor responseDescriptorWithMapping:categoryMapping
pathPattern:#"getAllCategories/:categoryID"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]]];
2)Create Class with categoryID in it.
[CategoryRequest class]
3) create object of that class and set categoryID
CategoryRequest *categoryRequest = [CategoryRequest alloc] init];
categoryRequest.categoryID = #"123";
4)call getobject using that object
[manager getObject:categoryRequest
path:#"getAllCategories/123"
parameters:params
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"Success");
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"Failure");
}];
if another call is required to be made for same mapping create object of of category request class set new category id and call get object using that categoryresquest and required path patter.
My JSON response looks like this:
{
"enabled": false,
"number_of_articles": 20,
"new_users": [
{
"id": "5001",
"name": "Jimmy Valner"
},
],
"articles" : [
{
"id" : 33122,
"title" : "Something",
"authors": [
{
"id": "511",
"name": "Somebody"
},
{
"id": "51",
"name": "Somebody else"
}
}]
}
Not really a proper RESTful response I know. But I have to map it somehow to two different Entities (User and Article). A already have a working code for the articles part. It maps the Article and the Articles as a relation.
I'm doing it like this:
RKMapping *mapping = [MappingProvider articlesMapping];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor
responseDescriptorWithMapping:mapping
pathPattern:#"/articles/:userId/latest"
keyPath:#"articles"
statusCodes:statusCodeSet];
NSURLRequest *request = [[RKObjectManager sharedManager] requestWithObject:nil
method:RKRequestMethodGET
path:[NSString stringWithFormat:#"/articles/%#/latest", [[CredentialStore sharedInstance] userId]]
parameters:nil];
RKManagedObjectStore *store = [[DataModel sharedDataModel] objectStore];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request
responseDescriptors:#[responseDescriptor]];
operation.managedObjectCache = store.managedObjectCache;
operation.managedObjectContext = store.mainQueueManagedObjectContext;
[operation setCompletionBlockWithSuccess:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"%#", mappingResult.array);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"ERROR: %#", error);
NSLog(#"Response: %#", operation.HTTPRequestOperation.responseString);
}];
[operation start];
This is working as expected. I provide the mapping instructions in the + (RKMapping *)articlesMapping method. Even the authors part of the JSON response is mapped properly.
Now I want to map new_users array to a different entity (User obviously). How can I do that by not invoking another request to the server ? I had some success with creating another RKManagedObjectRequestOperation *operation but that of course triggers another network request to the server which I want to avoid.
Should I look for the solution here in the RKObjectManager or should I somehow change the keyPath to start at root level and the do the mapping for different entities in the + (RKMapping *) method (probably not) ?
And another question .. how can I access the enabled, number_of_articles properties from this JSON? Again, I'm not dealing with a nice structured Restful response, but I have to deal with it somehow. I only want to access this properties somehow ... no mapping to entities needed.
RKManagedObjectRequestOperation has the method initWithRequest:responseDescriptors: for a reason. Note specifically the 's' on responseDescriptors and the fact that you're passing an array. This means you can configure multiple response descriptors, each with the same path pattern but different key paths and RestKit will process each of them on the response received from the server.
Note that when you do this you probably want to change your NSLog(#"%#", mappingResult.array); because it would contain all of the top level items in the list. Instead, you should probably use NSLog(#"%#", mappingResult.dictionary);