RESTEasy
What is RESTEasy?
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.
The "dual development" problem
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.
The prototyping problem
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.
The testing problem
Writing robust tests that interact with an API properly is HARD. This is due to several reasons:
- If you want to test abnormal behavior like latency swings or timeouts then you either need to get a test enviornment for your API that supports it (good luck convincing your backend team to support that, especially for more than one-off testing) or you need to use some kind of freakish proxy that messes with your NSURLRequests. Not fun or easy.
- If you are a good coder and you implement thorough testing in your app then you are going to be doing tests that rely on interactions with your API at some point either directly or indirectly. But what API endpoint are you going to use? You can't use your production service for anything but read-only data (or you will be populating your production system with a lot of junk) and test systems aren't exactly known for uptime or latency guarantees. You could stub out the services for your tests using a proxy and some static returns but this can be a lot of work to do right, especially if you want to simulate real network response times.
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.
You can RESTEasy
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...
Beginner stuff
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.
Setup the server with a simple resource
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people" model:@{
@"name": @(TGPropertyTypeString),
@"numberOfKids": @(TGPropertyTypeInteger),
@"kilometersWalked": @(TGPropertyTypeFloatingPoint),
@"avatar": @(TGPropertyTypeBlob)
}];
[[TGRESTServer sharedServer] addResource:people];
[[TGRESTServer sharedServer] startServerWithOptions:nil];
Our server is now ready to accept requests!
Basic CRUD
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.
Loading data
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?
Relational data and routes
You can set one-to-many relations between your resources really easily. All you have to do is something like this:
NSDictionary *peopleModel = @{
@"name": @(TGPropertyTypeString),
@"numberOfKids": @(TGPropertyTypeInteger),
@"kilometersWalked": @(TGPropertyTypeFloatingPoint),
@"avatar": @(TGPropertyTypeBlob)
};
NSDictionary *carsModel = @{
@"name": @(TGPropertyTypeString),
@"color": @(TGPropertyTypeString)
};
TGRESTResource *people = [TGRESTResource newResourceWithName:@"people"
model:peopleModel];
TGRESTResource *cars = [TGRESTResource newResourceWithName:@"cars"
model:carsModel
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).
Intermediate stuff
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.
Customizing the request/response representations
So your data doesn't exactly match the JSON representations that your server provides? There are a few reasons this might happen:
- Property names are just plain different.
- Certain properties are interpreted or calculated at runtime.
- You have structure to your JSON which isn't directly connected to the underlying data
- Cthulhu knows why but how do I not have to think about it?
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.
Loading a custom serializer
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.
Default serializer
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.
Resource serializer
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];
Setting latency minimum and maximums
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.
Advanced stuff
Really want to hack on RESTEasy? Well there are a few other things you can do.
Custom controllers
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.
Different store types
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
.
Usage
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
.
Documentation
Available as a Cocoadocs docset or you can download the repo and run 'rake docs:generate'. All of the classes are fully documented.
Example App
Have a look at the /Example folder.
Requirements
Designed for OSX 10.8 and iOS 6.0 and above.
Installation
RESTEasy is available through CocoaPods, to install it simply add the following line to your Podfile:
pod "RESTEasy"
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"
Acknowledgements
This library would not have been possible without a couple of fantastic subprojects that it makes use of.
- GCDWebServer GCD based HTTP server which is used as the underpinning for both the persisting and non-persisting REST server components.
- sqlite Used as the embedded database for the persisting REST server option.
- FMDB Objective-C wrapper that makes interacting with sqlite a lot more straightforward.
Author
John Tumminaro, [email protected]
License
RESTEasy is available under the MIT license. See the LICENSE file for more info.