Restkit v0.20 Dynamic mapping to NSDictionary - ios

In Restkit v0.10 a mapping to a NSDictionary could be done with the following code taken from https://stackoverflow.com/a/12057398/871459
RKObjectMapping* mapping = [RKDynamicObjectMapping dynamicMapping];
mapping.objectMappingForDataBlock = ^(id data) {
NSDictionary* object = [data objectForKey: #"object"];
NSArray* keys = [object allKeys];
RKObjectMapping* dataMapping = [RKObjectMapping objectMapping];
//Use the keys to define mapping
return dataMapping;
};
In v0.20 this isn't anymore possible. How can I implement the same on Restkit v0.20?

In 0.20, they use RKDynamicMapping instead of RKDynamicObjectMapping. I've not tested yet, but the answer is probably here:
RKDynamicMapping* dynamicMapping = [RKDynamicMapping new];
[dynamicMapping setObjectMappingForRepresentationBlock:RKObjectMapping *^(id data) {
NSDictionary* object = [data objectForKey: #"object"];
NSArray* keys = [object allKeys];
RKObjectMapping* dataMapping = [RKObjectMapping objectMapping];
//Use the keys to define mapping
return dataMapping;
}];
Or take a look at their tutorial for 0.20:

Related

How do I ignore empty properties on posted objects using restkit

I am reviving an old project that originally used RestKit 0.10, and now am using RestKit 0.24. The old version still works, but unfortunately RestKit 0.10 is not 64-bit compatible and hence does not submit to the AppStore (and it is certainly time to update anyway).
I cannot get an object to post correctly. In RestKit 0.10, properties without values were not sent to the server, whereas it seems in RestKit 0.20 they are. I have tried explicitly setting assignsDefaultValueForMissingAttributes to NO, but it doesn't seem to make a difference.
The server expects the following format:
{"response": {"assessment_id":"1","time_taken":"60"},
"answer": [
{"question_number": 1, "answer_value": 3},
{"question_number": 2, "answer_value": 2},
{"question_number": 3, "answer_value": 1},
]
}
I have set up an object CompletedAssessment which contains a Response object and an array of Answer objects. (Note that when these objects are received from the server, many more properties need to be received than need to be sent).
#interface CompletedAssessment : NSObject {
Response *response;
NSArray *answers;
}
#interface Answer : NSObject {
NSNumber *identifier;
NSNumber *responseId;
NSNumber *questionNumber;
NSString *answerHistory;
NSString *answerValue;
NSString *answerText;
NSNumber *timeTaken;
}
#interface Response : NSObject {
NSNumber *identifier;
NSNumber *assessmentId;
NSNumber *timeTaken;
NSNumber *clientId;
NSString *assessmentShortName;
NSString *score;
NSString *interpretation;
NSString *dateCreated;
NSString *localTime;
}
I set the mapping up as follows:
RKObjectMapping *answerMapping = [RKObjectMapping mappingForClass:[Answer class]];
answerMapping.assignsDefaultValueForMissingAttributes = NO;
[answerMapping addAttributeMappingsFromDictionary:#{
#"id": #"identifier",
#"response_id": #"responseId",
#"question_number": #"questionNumber",
#"answer_history": #"answerHistory",
#"answer_value": #"answerValue",
#"answer_text": #"answerText",
#"time_taken": #"timeTaken"
}];
RKObjectMapping *responseMapping = [RKObjectMapping mappingForClass:[Response class]];
responseMapping.assignsDefaultValueForMissingAttributes = NO;
[responseMapping addAttributeMappingsFromDictionary:#{
#"id": #"identifier",
#"client_id": #"clientId",
#"assessment_id": #"assessmentId",
#"time_taken": #"timeTaken",
#"score": #"score",
#"assessment_short_name": #"assessmentShortName",
#"interpretation": #"interpretation",
#"created": #"dateCreated",
#"local_time": #"localTime"
}];
RKObjectMapping *completedAssessmentMapping = [RKObjectMapping mappingForClass:[CompletedAssessment class]];
completedAssessmentMapping.assignsDefaultValueForMissingAttributes = NO;
[completedAssessmentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"response" toKeyPath:#"response" withMapping:responseMapping]];
[completedAssessmentMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"answer" toKeyPath:#"answers" withMapping:answerMapping]];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:completedAssessmentMapping method:RKRequestMethodGET pathPattern:nil keyPath:#"data.completedAssessment" statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)];
[[RKObjectManager sharedManager] addResponseDescriptor:responseDescriptor];
RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[completedAssessmentMapping inverseMapping] objectClass:[CompletedAssessment class] rootKeyPath:nil method:RKRequestMethodPOST];
[[RKObjectManager sharedManager] addRequestDescriptor:requestDescriptor];
[objectManager.router.routeSet addRoute:[RKRoute
routeWithClass:[CompletedAssessment class]
pathPattern:#"clients/:response.clientId/responses"
method:RKRequestMethodPOST]] ;
Logging reveals the end JSON appears in this format:
{"response":
{"interpretation":null,"id":null,"score":null,"client_id":15,"local_time":"2015-8-6 13:8:34","time_taken":5,"assessment_short_name":null,"assessment_id":8,"created":null},
"answer":[
{"answer_value":"0","id":null,"answer_text":null,"answer_history":null,"time_taken":null,"response_id":null,"question_number":1},
{"answer_value":"1","id":null,"answer_text":null,"answer_history":null,"time_taken":null,"response_id":null,"question_number":2}
]}
And RestKit logging confirms the null mapping:
restkit.object_mapping:RKMappingOperation.m:873 Mapped relationship object from keyPath 'response' to 'response'. Value: {
"assessment_id" = 8;
"assessment_short_name" = "<null>";
"client_id" = 15;
created = "<null>";
id = "<null>";
interpretation = "<null>";
"local_time" = "2015-8-6 13:8:34";
score = "<null>";
"time_taken" = 5;
}
restkit.object_mapping:RKMappingOperation.m:715 Mapped attribute value from keyPath 'identifier' to 'id'. Value: (null)
...
Please help!
You are creating a new mapping calling [selfCompletedAssessmentMapping inverseMapping] in this line:
RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:[selfCompletedAssessmentMapping inverseMapping] objectClass:[CompletedAssessment class] rootKeyPath:nil method:RKRequestMethodPOST];
Save it to a variable and assign assignsDefaultValueForMissingAttributes to NO before creating the descriptor:
RKObjectMapping *requestMapping = [selfCompletedAssessmentMapping inverseMapping];
requestMapping.assignsDefaultValueForMissingAttributes = NO;
RKRequestDescriptor *requestDescriptor = [RKRequestDescriptor requestDescriptorWithMapping:requestMapping objectClass:[CompletedAssessment class] rootKeyPath:nil method:RKRequestMethodPOST];

Match JSON using RestKit using KVC Validation

I have this JSON (an array of dictionaries):
[
{"id":"BTCLTC","last":"89.767","high":"96.185","low":"25.000","bid":"89.729","ask":"91.320","volume":"29.78918","scale":3},
{"id":"BTCUSD","last":"443.799","high":"444.092","low":"394.570","bid":"439.110","ask":"446.760","volume":"4.68266","scale":3},
{"id":"BTCXRP","last":"98101.500","high":"98101.500","low":"86000.000","bid":"94999.050","ask":"97499.900","volume":"21.66779","scale":3}
]
And I would like to use an RKObjectMappingMatcher to match against "id":"BTCUSD" for example.
EDIT:
Thanks to Wain's comment below, using an RKObjectMapping was enough for KVC validation to work. The validation method is automatically called by RestKit.
My code thus far looks like:
RKObjectMapping *defaultMapping = [RKObjectMapping mappingForClass:class];
NSDictionary *attributeMappings = [query objectForKey:#"attributeMappings"];
[defaultMapping addAttributeMappingsFromDictionary:attributeMappings];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping method:RKRequestMethodFromString(#"GET") pathPattern:#"/api/v1/markets" keyPath:#"" statusCodes:[NSIndexSet indexSetWithIndex:RKStatusCodeClassSuccessful]];
RKObjectManager *manager = [RKObjectManager managerWithBaseURL:url];
[manager addResponseDescriptor:responseDescriptor];
I've added what I think is necessary for validation for an NSObject.
- (BOOL)validateCurrencyConversionID:(id *)ioValue error:(NSError **)outError
{
if ([(NSString*)*ioValue length] == 0)
{
*outError = [NSError errorWithDomain:RKErrorDomain code:100 userInfo:#{#"description":#"Empty string"}];
return NO;
}
else if ([((NSString*)*ioValue) rangeOfString:#"USD"].location == NSNotFound)
{
*outError = [NSError errorWithDomain:RKErrorDomain code:101 userInfo:#{#"description":#"Doesn't contain USD"}];
return NO;
}
return YES;
}
Don't use this approach, because you can't suitably index into the array and as a result you're processing an array where you expect a string.
Instead, allow the mapping to run for each and then use KVC validation to abort the mapping for the items you don't want.

RestKit - Exclude attribute mapping with nil value from RKRequestDescriptor

The problem is that I need to remove the attribute #"unitprice" entirely from the payload when the value is nil, but keep it in there if it has a value in the request. So the payload for OrderLine would look as follows accordingly: #{"id":#"orderlineId", #"unitprice":#"unitprice"} OR #{"id":#"orderlineId"} Please note that the mapping is a one-to-many relationship. Is it possible to do this? Your help is really appreciated thank you!
/*
requestDescriptor
*/
+(RKObjectMapping*) getSalesOrderMapping:(RKRequestMethod)method {
RKEntityMapping *requestMapping = [RKEntityMapping mappingForEntityForName:#"SalesOrder"
inManagedObjectStore:[RKManagedObjectStore defaultStore]];
RKEntityMapping *orderLinesMapping = [RKEntityMapping mappingForEntityForName:#"OrderLine"
inManagedObjectStore:[RKManagedObjectStore defaultStore]];
requestMapping.identificationAttributes = #[ #"salesOrderId" ];
NSMutableDictionary *attributeMappings = [NSMutableDictionary dictionaryWithDictionary:#{
#"id": #"salesOrderId",
}];
NSDictionary *orderLineAttributeMappings = #{
#"id": #"orderlineId",
#"unitprice": #"unitPrice"
};
[orderLinesMapping addAttributeMappingsFromDictionary:orderLineAttributeMappings];
[requestMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"lines"
toKeyPath:#"salesOrderToOrderLines"
withMapping:orderLinesMapping]];
return requestMapping;
}
Set assignsDefaultValueForMissingAttributes to NO on the mapping. You shouldn't need 2 different mappings (unless you do want to include nil in the JSON for some attributes).
It seems impossible to implement dynamic request descriptor with 1-to-many relationship. So I had to build NSMutableDictionary manually by checking against nil value and then add the properties with its values like the following
[objectManager postObject:nil
path:#"/salesorder"
parameters:[self customSalesOrderRequestMapping]
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {... }
- (NSMutableDictionary *) customSalesOrderRequestMapping {
customSalesOrderAttributeDictionary = [NSMutableDictionary new];
if (selectedSalesOrder.salesOrderId) [customSalesOrderAttributeDictionary addEntriesFromDictionary:#{#"id": selectedSalesOrder.salesOrderId}];
if (selectedSalesOrder.salesOrderToOrderLines.count > 0) {
NSMutableArray *orderLinesMutableArray = [NSMutableArray new];
for (OrderLine *orderLine in selectedSalesOrder.salesOrderToOrderLines)
{
NSMutableDictionary *orderLineMutableDictionary = [NSMutableDictionary new];
if (orderLine.orderlineId) [orderLineMutableDictionary addEntriesFromDictionary:#{#"id": orderLine.orderlineId}];
if (orderLine.unitPrice) [orderLineMutableDictionary addEntriesFromDictionary:#{#"unitprice": orderLine.unitPrice}];
[orderLinesMutableArray addObject:orderLineMutableDictionary];
}
[customSalesOrderAttributeDictionary addEntriesFromDictionary:#{#"lines": orderLinesMutableArray}];
}
return customSalesOrderAttributeDictionary;
}

Map JSON "Associative array" into CoreData with RestKit

I need to map JSON associative array of objects with RestKit(iOS).
It looks like object with properties 135,145,423 and objects on it.
{
"135": {
"name" : "Object1",
"property1" : "Value1",
"anotherProperty1" : "Value2"
},
"145": {
"name": "Object2",
"property1" : "Value1",
"anotherProperty1" : "Value2"
},
"423": {
"name": "Object3",
"property1" : "Value1",
"anotherProperty1" : "Value2"
}
}
I've got mapping for single object that works.
Mapping performs to CoreData.
The only solution i have is to convert associative array to ordinary array and place number to "id" field, but i don't think it's elegant solution.
Is there any right way to perform such mapping directly with RestKit?
Here's the solution for my situation.
NSIndexSet *statusCodes = RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful); // Anything in 2xx
// 1. Create dynamic mapping
RKDynamicMapping* dynamicMapping = [[RKDynamicMapping alloc] init];
// 2. Process every entry separately
dynamicMapping.forceCollectionMapping = YES;
// 3. Set mappings for every object
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
// 4. Mapping to Core Data (Can be replaced with RKObjectMapping if there's no need of CodeData)
RKEntityMapping *singleRouteMapping = [RKEntityMapping mappingForEntityForName:#"Object" inManagedObjectStore:managedObjectStore];
// 5. Walking through all keys (but with dynamicMapping.forceCollectionMapping = YES) there'll be only one. It's better to refactor it.
for (NSString *key in representation) {
// 6. Set mappings for every property exect 'id'
[singleRouteMapping addAttributeMappingsFromDictionary:#{
[NSString stringWithFormat: #"%#.name", key]: #"name",
[NSString stringWithFormat: #"%#.property1", key]: #"property1",
[NSString stringWithFormat: #"%#.anotherProperty1", key]: #"anotherProperty1"
}];
}
// 7. Map 'id' property at last
[singleRouteMapping addAttributeMappingFromKeyOfRepresentationToAttribute: #"id"];
return singleRouteMapping;
}];
RKResponseDescriptor *pluralDescriptor = [RKResponseDescriptor responseDescriptorWithMapping: dynamicMapping
pathPattern: #"/api/objects"
keyPath: nil
statusCodes: statusCodes];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.somesite.com/api/objects"]];
RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithRequest:request responseDescriptors:#[pluralDescriptor]];
You need to use a dynamic mapping, where the mapping is created specifically for the received keys in the dictionary. You don't say what the destination object is or what your mappings are so this is a general example (for Core Data, but can be changed to plain objects):
RKDynamicMapping* dynamicMapping = [[RKDynamicMapping alloc] init];
[dynamicMapping setObjectMappingForRepresentationBlock:^RKObjectMapping *(id representation) {
RKEntityMapping* typeMapping = [RKEntityMapping mappingForEntityForName:#"..." inManagedObjectStore:objectStore];
for (NSString *key in representation) {
NSDictionary *type = [representation objectForKey:key];
[typeMapping addAttributeMappingsFromDictionary:#{
[NSString stringWithFormat:#"%#.name", key]: #"name"}];
}
return typeMapping;
}];
RKResponseDescriptor *responseDescriptor = [RKResponseDescriptor responseDescriptorWithMapping:dynamicMapping
pathPattern:...
keyPath:nil
statusCodes:[NSIndexSet indexSetWithIndex:200]];
This basically strips out the numbers and throws them away. You could include them if required by configuring the dynamic mapping.

RestKit 0.20-pre3 with RKXMLReaderSerialization and XMLReader

I'm trying to use RestKit 0.20-pre3 together with RKXMLReaderSerialization and XMLReader in order to map a XML response from a WebService like this:
<ArrayOfAddressBookItem xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://tempuri.org/">
<AddressBookItem>
<CommonName>xxxxxxxxxx</CommonName>
<OU>xxxxxx</OU>
<Name>xxxxxx</Name>
<LastName>xxxxxxxxxx</LastName>
<Service>xxxxxxxxxx</Service>
<Email>xxxxxxxxxxxx</Email>
<InternalPhoneNumber>xxxxxxxxxxx</InternalPhoneNumber>
<ExternalPhoneNumber>xxxxxxxxxxx</ExternalPhoneNumber>
<Mobile>xxxxxxxxxxx</Mobile>
<Street>xxxxxxxxxxx</Street>
<PostalCode>xxxxxxxxxxx</PostalCode>
<City>xxxxxxx</City>
<County>xxxxxxxxx</County>
<SupervisorCommonName>
xxxxxxxxxxxxx
</SupervisorCommonName>
<SupervisorLastName>xxxxxxxxxx</SupervisorLastName>
</AddressBookItem>
<AddressBookItem>
<CommonName>yyyyyyyyyyyy</CommonName>
<OU>
yyyyyyyyyyyyy
</OU>
<Name>yyyyyyyyy</Name>
<LastName>yyyyyyyy</LastName>
<Service>yyyyyyyyyy</Service>
<Email>yyyyyyyyyy</Email>
<InternalPhoneNumber>yyyyyyyyy</InternalPhoneNumber>
<ExternalPhoneNumber>yyyyyyyy</ExternalPhoneNumber>
<Street>yyyyyyyyyyy</Street>
<PostalCode>yyyyyy</PostalCode>
<City>yyyyyy</City>
<County>yyyyyyyy</County>
<SupervisorCommonName>
yyyyyyyyyyy
</SupervisorCommonName>
<SupervisorLastName>yyyyyy</SupervisorLastName>
</AddressBookItem>
<AddressBookItem>
....
</AddressBookItem>
<AddressBookItem>
</ArrayOfAddressBookItem>
In the App Delegation code:
[RKMIMETypeSerialization registerClass:[RKXMLReaderSerialization class] forMIMEType:#"application/xml"];
AFHTTPClient *httpClient = [AFHTTPClient clientWithBaseURL:[NSURL URLWithString:#"http://thehostaddress/mywebserviceurl/"]];
httpClient.parameterEncoding = AFFormURLParameterEncoding;
RKObjectManager *objManager = [[RKObjectManager alloc] initWithHTTPClient:httpClient];
[objManager setAcceptHeaderWithMIMEType:RKMIMETypeTextXML];
objManager.requestSerializationMIMEType = RKMIMETypeTextXML;
RKObjectMapping *personMapping = [RKObjectMapping mappingForClass:[PersonItem class]];
[personMapping addAttributeMappingsFromDictionary:#{#"CommonName" : #"commonName", #"OU" : #"ou", #"Name" : #"name", #"LastName" : #"lastName", #"Service" : #"service", #"Email" : #"eMail", #"InternalPhoneNumber" : #"internalPhoneNumber", #"ExternalPhoneNumber" : #"externalPhoneNumber", #"Mobile" : #"mobilePhoneNumber", #"Street" : #"street", #"PostalCode" : #"postalCode", #"City" : #"city", #"County" : #"county", #"SupervisorCommonName" : #"supervisorCommonName", #"SupervisorLastName" : #"supervisorLastName"}];
RKResponseDescriptor *peopleResponse = [RKResponseDescriptor responseDescriptorWithMapping:personMapping pathPattern:#"/mywebserviceurl/" keyPath:#"ArrayOfAddressBookItem" statusCodes:[NSIndexSet indexSetWithIndex:200]];
[objManager addResponseDescriptor:peopleResponse];
later, when I want to get the objects:
[objManager getObjectsAtPath:#"/mywebserviceurl/" parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
NSLog(#"SUCCESS: %#", mappingResult);
_items = [[mappingResult array] mutableCopy];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
NSLog(#"ERROR: %#", error);
}];
I can see that the mapper gets the correct number of the Array elements, but for each field of the object, I cannot retrieve the values:
2012-12-10 19:02:53.370 GenPeople2[14240:1703] T restkit.object_mapping:RKMappingOperation.m:341 Mapping attribute value keyPath 'CommonName' to 'commonName'
2012-12-10 19:02:53.370 GenPeople2[14240:1703] T restkit.object_mapping:RKMappingOperation.m:228 Found transformable value at keyPath 'CommonName'. Transforming from type '__NSDictionaryM' to 'NSString'
2012-12-10 19:02:53.371 GenPeople2[14240:1703] T restkit.object_mapping:RKMappingOperation.m:360 Skipped mapping of attribute value from keyPath 'CommonName to keyPath 'commonName' -- value is unchanged ((null))
and the result in my objects are null values.
I saw that the XML parser gives me back this:
2012-12-10 19:02:53.371 GenPeople2[14240:1703] D restkit.object_mapping:RKMapperOperation.m:218 Asked to map source
object {
City = {
text = thecity;
};
CommonName = {
text = thename;
};
County = {
text = thecounty;
};
and so on....
How to map correctly the values in order to permit RestKit to retrieve the values in the NSDictionary for each field ?
Thank Richard for your feedback, but it didn't work as I would like.
Really simpler: using nested keypaths in source object to map, worked as a charm:
RKObjectMapping *abItemMapping = [RKObjectMapping mappingForClass:[AddressBookItem class]];
[abItemMapping addAttributeMappingsFromDictionary:#{#"CommonName.text" : #"commonName", #"OU.text" : #"ou", #"Name.text" : #"name", #"LastName.text" : #"lastName", #"Service.text" : #"service", #"Email.text" : #"email"}];
I map the child nodes of an element as their own objects. So OU for example would be represented by a mapping and relationship of it's own:
RKObjectMapping *baseValueMapping = [RKObjectMapping mappingForClass:[CHRValue class]];
[baseValueMapping addAttributeMappingsFromDictionary:#{#"text" : #"value"}];
RKRelationshipMapping *ouRelation = [RKRelationshipMapping relationshipMappingFromKeyPath:#"OU" toKeyPath:#"ou" withMapping:baseValueMapping];
RKRelationshipMapping *nameRelation = [RKRelationshipMapping relationshipMappingFromKeyPath:#"Name" toKeyPath:#"name" withMapping:baseValueMapping];
[addressBookMapping addPropertyMapping:ouRelation];
[addressBookMapping addPropertyMapping:nameRelation];
Where CHRValue has a property named "value" which is an NSString. Note you need to use "text" to refer to the value of a node.

Resources