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
}
}
Related
I'm trying to use RestKit to parse a local XML file into CoreData. I've done this successfully with online REST services, but parsing a local file or just parsing XML seems to be a completely different beast.
First I use a class named CoreDataHandler, which just encapsulates all the default CoreData stuff. So it's basically what xcode provides you with in the AppDelegate, put into a separate class.
What I've done so far:
CoreDataHandler* handler = [CoreDataHandler instance];
RKManagedObjectMappingOperationDataSource* dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:handler.managedObjectContext cache:sharedManager.managedObjectStore.managedObjectCache];
dataSource.operationQueue = [NSOperationQueue new];
NSString* filePath = [[NSBundle mainBundle] pathForResource:#"Seed_010000" ofType:#"xml"];
NSData *sourceObject = [NSData dataWithContentsOfFile:filePath];
RKObjectMapping* mapping = [self mapCourses];
RKMappingOperation* operation = [[RKMappingOperation alloc] initWithSourceObject:sourceObject destinationObject:nil mapping:mapping];
operation.dataSource = dataSource;
NSError* error = nil;
[operation performMapping:&error];
if( error == nil ) {
NSLog(#"%#", [operation mappingInfo]);
[handler saveContext];
} else {
NSLog(#"error: %#", error);
}
The mappings are created here:
+ (RKObjectMapping*) mapCourses {
RKEntityMapping *courseMapping = [RKEntityMapping mappingForClass:[Course class]];
[courseMapping addAttributeMappingsFromDictionary:#{ #"Name.text" : #"name" }];
RKEntityMapping* sectionMapping = [RKEntityMapping mappingForClass:[Section class]];
[sectionMapping addAttributeMappingsFromDictionary:#{ #"order" : #"order",
#"Icon.text" : #"icon",
#"Name.text" : #"name",
#"Description.text" : #"desc" }];
[courseMapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:#"Sections" toKeyPath:#"sections" withMapping:sectionMapping]];
return courseMapping;
}
The XML File looks like this:
<MyProject>
<Courses>
<Course>
<Name>Name of the Course</Name>
<Sections>
<Section order="0">
<Icon>ICON_00</Icon>
<Name>Name of the Section</Name>
<Description>Description of the Section</Description>
</Section>
</Sections>
</Course>
</Courses>
</MyProject>
And the error I get is this:
2015-10-20 12:01:37.515 Training[16058:5400082] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<NSConcreteData 0x7fdf39f2b650> valueForUndefinedKey:]: this class is not key value coding-compliant for the key Sections.'
The stack trace points at the [operation performMaping:&error]; line.
I hope anybody can shed some light on this.
EDIT: I changed the question, because the first error (about the wrong initialiser was due to my own stupidity. I used RKObjectMapping instead of RKEntityMapping, which you have to use when using CoreData.
You look to have a couple of problems. It seems that you're using 2 different managed object contexts, one from the rest kit stack and one from your own stack. You shouldn't be creating any context of your own.
For your error, it's because you're using RKObjectMapping where you should be using RKEntityMapping because you're using Core Data.
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.
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.
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.
I'm attempting to map the following object:
{"walletAccepted":false,"creditCardTypesAccepted":["visa","mastercard","discover","americanexpress"],"paypalAccepted":false}
To an object with identical attributes. However, the ResourcePath is dynamic, in that it's along the lines of /paymentmethods/zone/:internalZoneCode
Every attempt I've made at mapping it always ends up with "Encountered errors during mapping: Could not find an object mapping for keyPath: ''"
I think the issue is that there is no root key/key path. I've also attempted the following with no luck:
RKURL *rkUrl = [RKURL URLWithBaseURL:[RKClient sharedClient].baseURL resourcePath:[NSString stringWithFormat:#"/paymentoptions/zone/%#", zoneNumber]];
RKObjectLoader* loader = [[RKObjectLoader alloc] initWithURL:rkUrl mappingProvider:[RKObjectManager sharedManager].mappingProvider];
loader.method = RKRequestMethodGET;
loader.delegate = self;
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[PMZonePaymentMethods class]];
[loader send];
I found the way to make it happen is to configure the mapping, and add the mapping to the object manager without a key path or resourcek
So like this:
RKObjectMapping* zonePaymentMethodsMapping = [RKObjectMapping mappingForClass:[PMZonePaymentMethods class]];
[zonePaymentMethodsMapping mapKeyPath:#"walletAccepted" toAttribute:#"walletAvailable"];
[zonePaymentMethodsMapping mapKeyPath:#"creditCardTypesAccepted" toAttribute:#"cards"];
[zonePaymentMethodsMapping mapKeyPath:#"paypalAccepted" toAttribute:#"paypalAvailable"];
[[RKObjectManager sharedManager].mappingProvider addObjectMapping:zonePaymentMethodsMapping];
and use this to make the request:
RKURL *rkUrl = [RKURL URLWithBaseURL:[RKClient sharedClient].baseURL resourcePath:[NSString stringWithFormat:#"/paymentoptions/zone/%#", zoneNumber]];
RKObjectLoader* loader = [[RKObjectLoader alloc] initWithURL:rkUrl mappingProvider: [RKObjectManager sharedManager].mappingProvider];
loader.method = RKRequestMethodGET;
loader.delegate = self;
loader.objectMapping = [[RKObjectManager sharedManager].mappingProvider objectMappingForClass:[PMZonePaymentMethods class]];
[loader send];