TestsTested | ✓ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | Dec 2014 |
Maintained by Unclaimed.
As client developers a lot of our job is spent interacting with various webservices, usually of the RESTful type. There's a reason why AFNetworking is probably the most popular third party library in the Cocoa ecosphere (to the point where I can scarcely remember the last project I did that didn't use it) and all because it makes interacting with webservices that much easier.
However webservices integration still has a few thorns in client development that are hard to get around.
As nice as it would be to think that all client projects start with a finished API that's not often true. How often do you work on a client project where you have a dependency on an API that is under heavy development? Sure the general object data structure is probably known but it's very unlikely that you will have a fully stable API on day one of your development.
Furthermore once you start writing code that interacts with that API you are going to notice certain design changes that will make that API better. No matter how good of a backend engineering team you are working with they are pretty much designing their API in a vacuum until the first client starts integrating with it but sadly by that time it is often too late for substantive changes.
If you are anything like me then you might have solid Objective-C skills but your experience with backend code leaves a bit to be desired. When I need to prototype an API because I want to try a client idea I will often put together something simple using rails-api or sinatra and although these work pretty well its obviously not the sort of shift that I particularly enjoy when I am trying to prototype a client app since it forces me to switch between development environments constantly and those frameworks are much more geared towards building actual PRODUCTION applications so they are more complex then they need to be for simple prototyping jobs. Furthermore they are meant for developing general purpose APIs whereas if you are just trying to prototype something for an iOS app its probably more than you need.
I've played with several other frameworks for this purpose such as Deployd and even serving static JSON using a small sinatra instance but none of them were ideal for someone who really wants something that I can contain within my Objective-C projects.
Writing robust tests that interact with an API properly is HARD. This is due to several reasons:
Sadly most people either end up writing very brittle integration tests and then throw up their hands at all the ghost failures they see (sometimes missing actual bugs in their code in the process) or they just avoid tests that hit those webservices entirely. This might be why (or at least one of a few reasons) unit testing on iOS hasn't advanced to quite the degree that it has on a lot of server side languages.
RESTEasy is my proposed solution to these problems. Want a RESTful server setup quick? Just do this?
#import <RESTEasy/RESTEasy.h>
[[TGRESTServer sharedServer] startServerWithOptions:nil];
You now have a RESTful API server running inside your Objective-C client project! It can be reached via HTTP calls just like any other server and its ready to start accepting requests right away. But that's only the beginning of the power this unlocks...
Before I dive into everything that RESTEasy can do, let's start with a few basic examples and then I will show you how to bend this library to your will.
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people" model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"numberOfKids": [NSNumber numberWithInteger:TGPropertyTypeInteger],
@"kilometersWalked": [NSNumber numberWithInteger:TGPropertyTypeFloatingPoint],
@"avatar": [NSNumber numberWithInteger:TGPropertyTypeBlob]
}];
[[TGRESTServer sharedServer] addResource:people];
[[TGRESTServer sharedServer] startServerWithOptions:nil];
Our server is now ready to accept requests!
You can check that the server is working using curl if you like by doing the following:
curl http://localhost:8888/people
[]
An empty array? Well that's not very exciting but it is a start. So how do we get data in there? Well we could just start the normal REST way:
curl \
-X POST \
-H "Content-Type: application/json" \
-d '{"name":"john","numberOfKids":1}' \
http://10.0.1.66:8888/people
{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"john","avatar":null}
Huzzah! Our first object returned successfully. We could also repeat our first request and get this:
[{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"john","avatar":null}]
Ok then, let's try updating it so the name is "jeff" instead.
curl \
-X PUT \
-H "Content-Type: application/json" \
-d '{"name":"jeff"}' \
http://10.0.1.66:8888/people/1
{"numberOfKids":1,"id":1,"kilometersWalked":null,"name":"jeff","avatar":null}
Not bad. How about a delete?
curl \
-X DELETE \
http://10.0.1.66:8888/people/1
Works just like you'd expect.
Of course we don't want to have to load our entire dataset just with API calls. Fortunately RESTEasy has you covered with some very simple ways to load your sample data.
Let's say your data is in JSON with identical property names to the resource model you defined for the sake of this example--we can load it doing something like this:
// Let's assume this was assigned to the resource we created before
TGRESTResource *people = ...
// First lets get the JSON from disk and parse it
NSString *pathToJSON = [[NSBundle mainBundle] pathForResource:@"testdata" ofType:@"json"];
NSData *rawJSON = [NSData dataWithContentsOfFile:pathToJSON];
NSError *parsingError;
id json = [NSJSONSerialization JSONObjectWithData:rawJSON options:kNilOptions error:&parsingError];
if (!error) {
// Now pass it to the server
[[TGRESTServer sharedServer] addData:json forResource:people];
}
The objects that you pass will first be sanitized and harvested of any matching parameters and as long as at least one property name matches it will injest the entire JSON object and create data for each. Easy right?
You can set one-to-many relations between your resources really easily. All you have to do is something like this:
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people"
model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"numberOfKids": [NSNumber numberWithInteger:TGPropertyTypeInteger],
@"kilometersWalked": [NSNumber numberWithInteger:TGPropertyTypeFloatingPoint],
@"avatar": [NSNumber numberWithInteger:TGPropertyTypeBlob]
}];
TGRESTResource *cars = [TGRESTResource newResourceWithName:@"cars"
model:@{
@"name": [NSNumber numberWithInteger:TGPropertyTypeString],
@"color": [NSNumber numberWithInteger:TGPropertyTypeString]
}
actions:TGResourceRESTActionsDELETE | TGResourceRESTActionsGET | TGResourceRESTActionsPOST | TGResourceRESTActionsPUT
primaryKey:nil
parentResources:@[people]];
[[TGRESTServer sharedServer] addResource:people];
[[TGRESTServer sharedServer] addResource:cars];
And by doing that we get the following default routes created:
Resource | Verb | URI Pattern | Result |
---|---|---|---|
people | GET | /people | List of all people |
people | GET | /people/:id | Single person by id |
people | POST | /people | Creates a new person |
people | PUT | /people/:id | Updates a person by id |
people | DELETE | /people/:id | Deletes a person by id |
cars | GET | /cars | List of all cars |
cars | GET | /cars/:id | Single car by id |
cars | GET | /people/:id/cars | All cars for person by id |
cars | POST | /cars | Creates a new car |
cars | POST | /people/:id/cars | Creates a new car for person by id |
cars | PUT | /cars/:id | Updates a car by id |
cars | DELETE | /cars/:id | Deletes a car by id |
This follows a concept similar to shallow nesting as described here with a few minor differences (which mostly involve restriction of nested resources which is not the point of this library).
Ok the above should give you a pretty good idea of how to get started quickly. But what about customization?
Although RESTEasy is supposed to be about mocking webservices not creating exact replicas, there are a few areas where you might understandably need a little more than the basics. RESTEasy was architected to make this as simple as possible without jeprodizing the core mission of being approachable and dead simple to setup and start using.
So your data doesn't exactly match the JSON representations that your server provides? There are a few reasons this might happen:
Yes you could normalize the data before importing (and that's cerainly an option) but if you want more control look no further than the TGRESTSerializer
protocol for all your data representation needs.
This protocol is pretty simple, it defines three class methods:
+ (id)dataWithSingularObject:(NSDictionary *)object resource:(TGRESTResource *)resource;
+ (id)dataWithCollection:(NSArray *)collection resource:(TGRESTResource *)resource;
+ (NSDictionary *)requestParametersWithBody:(NSDictionary *)body resource:(TGRESTResource *)resource;
The first two are for generating responses from either a single object or collection of objects after they are retrieved from the datastore and the third method provides the paramters of an incoming request which you can normalize however you like. It's actually wrong to call this a serializer since it doesn't actually serialize/deserialize anything (although when you think about sequencing of events these methods are invoked either directly before or directly after "real" serialization), it is instead a way of morphing the data into whatever you want it to look like before it goes in or comes out of the server resource controller.
You can change property names, formatting, inject default or static values, anything you like. The philosophy here is that if you are going to need to perform an ugly hack to get your data representations to look right then lets do a proper ugly hack and isolate it from the rest of the framework.
There is a default implementation of a class that conforms to the TGRESTSerializer
protocol called TGRESTDefaultSerializer
. It is really instructive to look at its implementation file.
#import "TGRESTDefaultSerializer.h"
@implementation TGRESTDefaultSerializer
+ (id)dataWithSingularObject:(NSDictionary *)object resource:(TGRESTResource *)resource
{
NSParameterAssert(object);
NSParameterAssert(resource);
return object;
}
+ (id)dataWithCollection:(NSArray *)collection resource:(TGRESTResource *)resource
{
NSParameterAssert(collection);
NSParameterAssert(resource);
return collection;
}
+ (NSDictionary *)requestParametersWithBody:(NSDictionary *)body resource:(TGRESTResource *)resource
{
NSParameterAssert(resource);
return body;
}
@end
So it does absolutely nothing which is exactly the point. If you want to customize the requests or responses to make them fit the way an existing API works you can do so and you don't need to touch your data representations as they will be blissfully ignorant of all of this.
Once you create a class that implements the TGRESTSerializer
protocol you need to load it onto the server. There are two ways of doing this.
This is the serializer that is used if there are no specified resource serializers. If you want to contain it all in one class with a single set of rules this is an easy way to go, just remember that unlike the resource serializers you can only set this when starting the server up.
To set the default serializer do this:
NSDictionary *options = @{TGRESTServerDefaultSerializerClassOptionKey: [MyCustomSerializer class]};
[[TGRESTServer sharedServer] startServerWithOptions:options];
Notice that you are passing the class of your custom serializer not an instance.
You can set a resource serializer anytime you like, even when the server is running. The way it works is that the resource controller will look for a serializer for the resource first and only go to the default serializer if it can't find one. So this lets you assign custom serializers where necessary but you can always count on the default being there if you haven't defined one.
Setting a serializer for a resource is as easy as:
[[TGRESTServer sharedServer] setSerializerClass:[MyCustomSerializer class] forResource:people];
Then to remove it:
[[TGRESTServer sharedServer] removeCustomSerializerForResource:people];
In order to perform proper simulations of network tests it is often necessary to use things like link conditioner. This works adaquately but since that throttles on the client it requires network connections to remote services. Sure you could just use link conditioner with RESTEasy but there is a more straightforward solution.
The server configuration includes the ability to set latency minium and maximums like so:
NSDictionary *serverOptions = @{
TGLatencyRangeMinimumOptionKey: @1.0,
TGLatencyRangeMaximumOptionKey: @2.0
};
[[TGRESTServer sharedServer] startServerWithOptions:serverOptions];
This will make it so that responses are artificially throtled so that they return with a random response time within AT LEAST the range specified (however obviously it could go higher if the range is low and the request takes a long time for whatever reason). It's good to set this to simulate real network requests as local calls tend to return in the 10ms timeframe if you don't simulate a delay.
Really want to hack on RESTEasy? Well there are a few other things you can do.
You want to do something beyond basic CRUD on your resources? While RESTEasy is not and will never be Rails it does have the ability to swap out the default controller for another one. This won't change the default routes but what it will do is give you full customizability over the controller actions (Index, Show, Create, Update, Destroy). Check out the TGRESTController
protocol or the TGRESTDefaultController
class for some ideas on where you can go with this as you can either subclass TGRESTDefaultController
if you want to keep the super CRUD methods or create your own conforming controller.
Once you have it you just load it onto the server much like you do with the custom serializers.
NSDictionary *options = @{TGRESTServerControllerClassOptionKey: [MyCustomController class]};
[[TGRESTServer sharedServer] startServerWithOptions:options];
Just be sure this is really what you want as this is an advanced feature that most people won't need or want to bother with. You should have exhausted what you can do with a custom TGRESTSerializer
before you even think about using this.
By default there are two store types: in-memory and sqlite and they both have their uses. But you'll notice that there are both subclasses of an abstract superclass called TGRESTStore
. If you want to hook up your REST API to something else (anything from a JSON file to a remote webservice) you can do so by creating a custom store that implements all the methods on TGRESTStore
.
- (NSUInteger)countOfObjectsForResource:(TGRESTResource *)resource;
- (NSDictionary *)getDataForObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
error:(NSError * __autoreleasing *)error;
- (NSArray *)getDataForObjectsOfResource:(TGRESTResource *)resource
withParent:(TGRESTResource *)parent
parentPrimaryKey:(NSString *)key
error:(NSError * __autoreleasing *)error;
- (NSArray *)getAllObjectsForResource:(TGRESTResource *)resource
error:(NSError * __autoreleasing *)error;
- (NSDictionary *)createNewObjectForResource:(TGRESTResource *)resource
withProperties:(NSDictionary *)properties
error:(NSError * __autoreleasing *)error;
- (NSDictionary *)modifyObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
withProperties:(NSDictionary *)properties
error:(NSError * __autoreleasing *)error;
- (BOOL)deleteObjectOfResource:(TGRESTResource *)resource
withPrimaryKey:(NSString *)primaryKey
error:(NSError * __autoreleasing *)error;
- (void)addResource:(TGRESTResource *)resource;
- (void)dropResource:(TGRESTResource *)resource;
If you want more details on implementing your own concrete store class check out the documentation for TGRESTStore
as well as both of the existing implementations TGRESTInMemoryStore
and TGRESTSqliteStore
.
If you want to play with the example app, run the test suite yourself or submit a pull request then clone the repo and run pod install
from the root directory first then open RESTEasy.xcworkspace
.
Available as a Cocoadocs docset or you can download the repo and run 'rake docs:generate'. All of the classes are fully documented.
Have a look at the /Example folder.
Designed for OSX 10.8 and iOS 6.0 and above.
Don't need or want persistence? You can install just the core files that rely on the in memory store only and don't have depdencies on FMDB or sqlite by using the 'core' subspec like so:
pod "RESTEasy/core"
This library would not have been possible without a couple of fantastic subprojects that it makes use of.
John Tumminaro, [email protected]
RESTEasy is available under the MIT license. See the LICENSE file for more info.