#Polymer
Polymer is an endpoint focused networking library for Objective-C and Swift that is meant to make interaction with REST webservices simple, fast, and fun! By treating the endpoints of a webservice as objects, it makes interaction more straightforward and readable while still leveraging and simplifying some of the amazing mapping technologies we've grown used to in consuming apis.
The goal of this library is to be as minimalistic as possible while providing maximum customization. This is achieved by making transparent methods that can be easily overridden when necessary to handle edge cases and customize the behavior of an endpoint.
###Genome
Polymer features the mapping library Genome
###AFNetworking
Polymer relies on AFNetworking for its network operations
Documentation
Initial Setup
Getting Started Guide -- Spotify Search
Models
Endpoints
Use
Endpoints
Base Endpoint
Base Url
Header Fields
Acceptable Content Types
Individual Endpoint
Return Class
Endpoint Url
Slug Mapping
Response Key Path
Serializers
Response Serializer
Request Serializer
Append Header
Transform Response
Networking - Examples
GET
POST
PUT
PATCH
DELETE
#Initial Setup
If you wish to install the library manually, you'll need to also include AFNetworking and Genome
It is highly recommended that you install Polymer through cocoapods. Here is a personal cocoapods reference just in case it may be of use: Cocoapods Setup Guide
Podfile: pod 'Polymer'
Import: #import <Polymer/PLYEndpoint.h>
#Getting Started
The best way to describe Polymer is to show how it is used. Let's query some artists from the spotify web api. Here's an example response from this endpoint.
{
"artists" : {
"href" : "https://api.spotify.com/v1/search?query=tania+bowra&offset=0&limit=20&type=artist",
"items" : [ {
"external_urls" : {
"spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q"
},
"followers" : {
"href" : null,
"total" : 12
},
"genres" : [ ],
"href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q",
"id" : "08td7MxkoHQkXnWAYD8d6Q",
"images" : [ {
"height" : 640,
"url" : "https://i.scdn.co/image/f2798ddab0c7b76dc2d270b65c4f67ddef7f6718",
"width" : 640
}, {
"height" : 300,
"url" : "https://i.scdn.co/image/b414091165ea0f4172089c2fc67bb35aa37cfc55",
"width" : 300
}, {
"height" : 64,
"url" : "https://i.scdn.co/image/8522fc78be4bf4e83fea8e67bb742e7d3dfe21b4",
"width" : 64
} ],
"name" : "Tania Bowra",
"popularity" : 4,
"type" : "artist",
"uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q"
} ],
"limit" : 20,
"next" : null,
"offset" : 0,
"previous" : null,
"total" : 1
}
}
For our example, the only thing we really care about is the artist objects located at the keypath artists.items
. We will use this keypath later. First, let's isolate our artist object for mapping, it looks like this:
#####SpotifyArtist Json Representation
{
"external_urls" : {
"spotify" : "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q"
},
"followers" : {
"href" : null,
"total" : 12
},
"genres" : [ ],
"href" : "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q",
"id" : "08td7MxkoHQkXnWAYD8d6Q",
"images" : [ {
"height" : 640,
"url" : "https://i.scdn.co/image/f2798ddab0c7b76dc2d270b65c4f67ddef7f6718",
"width" : 640
}, {
"height" : 300,
"url" : "https://i.scdn.co/image/b414091165ea0f4172089c2fc67bb35aa37cfc55",
"width" : 300
}, {
"height" : 64,
"url" : "https://i.scdn.co/image/8522fc78be4bf4e83fea8e67bb742e7d3dfe21b4",
"width" : 64
} ],
"name" : "Tania Bowra",
"popularity" : 4,
"type" : "artist",
"uri" : "spotify:artist:08td7MxkoHQkXnWAYD8d6Q"
}
Let's how it would look modeled as an ObjC object.
####Spotify Models
#####SpotifyArtist Model
The first thing we need to do is create our object and make sure it conforms to GenomeMappableObject
protocol. Here's how our object looks now.
SpotifyArtist.h
#import <Foundation/Foundation.h>
#import <Genome/Genome.h>
@interface SpotifyArtist : NSObject <GenomeMappableObject>
@end
Now let's fill in the properties that map to the JSON. Our final model header will look like this:
SpotifyArtist.h
#import <Foundation/Foundation.h>
#import <Genome/Genome.h>
@interface SpotifyArtist : NSObject <GenomeMappableObject>
@property (strong, nonatomic) NSURL *externalSpotifyUrl;
@property (nonatomic) NSInteger numberOfFollowers;
@property (strong, nonatomic) NSArray *genres;
@property (strong, nonatomic) NSURL *url;
@property (copy, nonatomic) NSString *identifier;
@property (strong, nonatomic) NSArray *images;
@property (copy, nonatomic) NSString *name;
@property (nonatomic) NSInteger popularity;
@property (copy, nonatomic) NSString *type;
@property (strong, nonatomic) NSURL *uri;
@end
GenomeMappableObject
protocol requires implementing an instance method that is called mapping
and returns an NSMutableDictionary
. This will be used under the hood when converting the JSON response to model objects. Modelling supports the following syntax:
mapping[@"<#propertyName#>"] = @"<#associatedJsonKeyPath#>";
This operation tries to be smart and if you have a property that is a class that also corresponds to a GenomeObject
, it will be mapped automatically. If your property is an array of GenomeObject
s, the type needs to be declared explicitly since this can't be discovered through introspection. To do this, you use the following syntax:
mapping[@"<#arrayPropertyName#>@<#ClassName#>"] = @"<#associatedJsonKeyPath#>";
The @
syntax is an important feature of Genome and it will be included quite often. If you would like to be a bit more type safe, you can use this convenience function to declare your keys:
propertyMap(@"<#propertyName#>", [<#classType#> class])
This is a bit safer way to map so that if you refactor your class names, you don't need to do a project search to replace your key mappings.
This syntax can also be used to declare a GenomeTransformer
to go along with the class. More on that later. Let's look at our mapping for SpotifyArtist
SpotifyArtist.m
#import "SpotifyArtist.h"
@implementation SpotifyArtist
+ (NSDictionary *)mapping {
NSMutableDictionary *mapping = [NSMutableDictionary dictionary];
// Note keypaths in associated JSON
mapping[@"externalSpotifyUrl"] = @"external_urls.spotify";
mapping[@"numberOfFollowers"] = @"followers.total";
mapping[@"genres"] = @"genres";
mapping[@"url"] = @"href";
mapping[@"identifier"] = @"id";
// Note array type specification
mapping[@"images@SpotifyImageRef"] = @"images";
mapping[@"name"] = @"name";
mapping[@"popularity"] = @"popularity";
mapping[@"type"] = @"type";
mapping[@"uri"] = @"uri";
return mapping;
}
@end
As you can see above, our images
property is an array and we are mapping its contents to SpotifyImageRef
models that we haven't created yet. Let's look at the json contained at the images
key:
"images" : [ {
"height" : 640,
"url" : "https://i.scdn.co/image/f2798ddab0c7b76dc2d270b65c4f67ddef7f6718",
"width" : 640
}, {
"height" : 300,
"url" : "https://i.scdn.co/image/b414091165ea0f4172089c2fc67bb35aa37cfc55",
"width" : 300
}, {
"height" : 64,
"url" : "https://i.scdn.co/image/8522fc78be4bf4e83fea8e67bb742e7d3dfe21b4",
"width" : 64
} ]
Let's create a model for the individual objects that looks like this:
#####SpotifyImageRef
SpotifyImageRef.h
#import <Foundation/Foundation.h>
#import <Genome/Genome.h>
@interface SpotifyImageRef : NSObject <GenomeObject>
@property (nonatomic) NSInteger height;
@property (nonatomic) NSInteger width;
@property (copy, nonatomic) NSURL *url;
@end
Note: You can declare GenomeObject
protocol in the implementation file if you prefer. This is more clear for examples.
SpotifyImageRef.m
#import "SpotifyImageRef.h"
@implementation SpotifyImageRef
+ (NSDictionary *)mapping {
NSMutableDictionary *mapping = [NSMutableDictionary dictionary];
mapping[@"height"] = @"height";
mapping[@"width"] = @"width";
mapping[@"url"] = @"url";
return mapping;
}
@end
This is a pretty straightforward object and our property names correspond directly with the JSON. As of now, it is still necessary to declare these properties in your mapping. This is done to allow absolute control over the operation.
That's it, our models are all set up, now we need to set up our endpoints for the spotify api.
####Spotify Endpoints
I prefer endpoints declared in a single file because it prevents having to add additional imports when endpoints are added and a lot of them end up being interdependent.
In your endpoints file, import <Polymer/PLYEndpoint.h>
SpotifyEndpoints.h
#import <Foundation/Foundation.h>
#import <Polymer/PLYEndpoint.h>
The first thing I'm going to do is declare a base endpoint. This is done to provide the base Url and any other request configurations you want for a given api.
#import <Foundation/Foundation.h>
#import <Polymer/PLYEndpoint.h>
@interface SpotifyBaseEndpoint : PLYEndpoint
@end
Now let's look at the implementation:
SpotifyEndpoints.m
#import "SpotifyEndpoints.h"
#import "SpotifyArtist.h"
@implementation SpotifyBaseEndpoint
- (NSString *)baseUrl {
return @"https://api.spotify.com/v1";
}
@end
Spotify is a modern and clean api, and most characteristics are able to be inferred quite easily, if you would like more control over your base endpoint, you can create something more complex by adding more method overrides. A more specified API might look something like this:
@implementation GHBaseEndpoint
- (NSSet *)acceptableContentTypes {
return [NSSet setWithObjects:@"text/html", @"application/json", nil];
}
- (AFHTTPRequestSerializer<AFURLRequestSerialization> *)requestSerializer {
return [AFJSONRequestSerializer serializer];
}
- (NSDictionary *)headerFields {
NSMutableDictionary *headerFields = [NSMutableDictionary dictionary];
headerFields[@"Accept"] = @"application/vnd.github.v3+json";
NSString *token = [storage accessToken];
if (token) {
NSString *tokenHeader = [NSString stringWithFormat:@"Token %@", token];
headerFields[GHNetworkingHeaderKeyAuthorization] = tokenHeader;
}
return headerFields;
}
- (NSString *)baseUrl {
return @"https://api.github.com";
}
@end
Ok, now back to Spotify. It's time to add a new endpoint for our search. This endpoint, and all future endpoints desiring this base url will subclass our spotify base endpoint.
Here's how our endpoints file looks after adding the search endpoint:
SpotifyEndpoints.h
#import <Foundation/Foundation.h>
#import <Polymer/PLYEndpoint.h>
@interface SpotifyBaseEndpoint : PLYEndpoint
@end
// Note subclass
@interface SpotifySearchEndpoint : SpotifyBaseEndpoint
@end
SpotifyEndpoints.m
#import "SpotifyEndpoints.h"
#import "SpotifyArtist.h"
@implementation SpotifyBaseEndpoint
- (NSString *)baseUrl {
return @"https://api.spotify.com/v1";
}
@end
@implementation SpotifySearchEndpoint
- (Class)returnClass {
return [SpotifyArtist class];
}
- (NSString *)endpointUrl {
return @"search";
}
- (NSString *)responseKeyPath {
return @"artists.items";
}
@end
An endpoint meant for use implements at minimum 3 methods. baseUrl
, endpointUrl
, and returnClass
. In SpotifySearchEndpoint
above, you'll notice that baseUrl
isn't overridden. This is because it subclasses from SpotifyBaseEndpoint
which overrides the baseUrl
. All future subclasses can inherit this base.
baseUrl
- The base url for the api. What the endpoints will be appended to.
endpointUrl
- The url for the endpoint. You can declare a more advanced endpoint by prefixing slugs w/ a colon :
. These can be smartly mapped from objects to generate endpoints. (more on slug mapping later).
responseKeyPath
- As we specified at the beginning, this is a simple example and we don't need all of the information from the response. We only want the array of artists located at the key path artists.items
. By declaring this in our endpoint, we're telling it. Fetch items from url endpoint search
, then from the response, get the object at keypath artists.items
. Then map the objects within that response to type SpotifyArtist
.
That's it, we're ready to use the search api!
####Use!
Everything is set up, let's get some objects down from the server! At minimum, the spotify search endpoint requires two parameters, query : q
and type artist
, album
, or track
. For our example, we're querying artists, so we'll have that for our type. Now we initialize our endpoint and call get.
PLYEndpoint *ep = [SpotifySearchEndpoint endpointWithParameters:@{@"q" : @"beyonce", @"type" : @"artist"}];
[ep getWithCompletion:^(id object, NSError *error) {
NSArray *artists = (NSArray *)object;
NSLog(@"Got artists: %@ w/ error: %@", artists, error);
}];
Because objective-c allows flexibility in type casting, we can skip the cast in the above example and replace our object with its type explicitly.
PLYEndpoint *ep = [SpotifySearchEndpoint endpointWithParameters:@{@"q" : @"beyonce", @"type" : @"artist"}];
[ep getWithCompletion:^(NSArray *artists, NSError *error) {
NSLog(@"Got artists: %@ w/ error: %@", artists, error);
}];
This can also be done to individual model objects, not just NSArray
s. The headers are heavily documented for more information!
###Endpoints
Think of your api's endpoint as an object, and this class as its model. It has the following components:
It starts with a set of base properties that are meant to be overridden in your endpoint subclass.
####Base Endpoint
When consuming a webservice, there is often a base set of configurations that apply to all endpoints. These overrides are often declared in a base class that is then subclassed by endpoints; however, these can always be overridden by an individual endpoint as necessary.
#####Base Url
This indicates the base Url that the endpoint Url should be appended to. It is common practice to override this in a base class for your api and subclass further for endpoints (see Getting Started).
######Objc
- (NSString *)baseUrl {
return @"http://api.somewebservice.com";
}
######Swift
override var baseUrl: String {
return "http://api.somewebservice.com"
}
#####Header Fields
This is where you can declare the header fields necessary when making web requests to the api. The most common use cases of this involve accept types and tokens. Again, it is common for this to exist in the base endpoint for a given api, but it can be overriddent for specific endpoints as necessary. A basic implementation can look something like this:
######ObjC
- (NSDictionary *)headerFields {
NSMutableDictionary *header = [NSMutableDictionary dictionary];
header[@"Accept"] = @"application/vnd.somewebservice.com+json; version=1";
header[@"Authorization"] = [NSString stringWithFormat:@"Token token=%@", MY_TOKEN];
return header;
}
######Swift
override var headerFields: [NSObject : AnyObject] {
var header: [NSObject : AnyObject] = [:]
header["Accept"] = "application/vnd.somewebservice.com+json; version=1"
header["Authorization"] = "Token token=\(MY_TOKEN)"
return header
}
#####Acceptable Content Types
You can use this to specify the content types to be accepted from a webservice. Again, this is often specified in the base class, but it can be overridden by individual endpoints as necessary.
Note: For modern webservices, it is often not necessary to override this function.
######ObjC
- (NSSet *)acceptableContentTypes {
return [NSSet setWithObjects:@"application/json", @"text/html", nil];
}
override var acceptableContentTypes: Set<NSObject> {
return Set(["application/json", "text/html"])
}
######Swift
override var acceptableContentTypes: Set<NSObject> {
return Set(["application/json", "text/html", "text/html; charset=utf-8"])
}
####Individual Endpoints
Once your base endpoint is defined, your individual endpoints should subclass that to specify individual behavior. Remember that for specific situations, each of the above methods can also be subclassed in your endpoint model.
#####Return Class
Use this to define what model the response for this endpoint should be mapped to. If this endpoint is an array response, the endpoint will return an array of this class.
NOTE: This class must conform to GenomeObject protocol
######ObjC
- (Class)returnClass {
return [Post class];
}
######Swift
override var returnClass: AnyClass {
return Post.self
}
#####Endpoint Url
This is where you declare the endpoint to append to the base url. You can also use this place to indicate slug paths to use when populating your url. A common implementation looks something like this:
######ObjC
- (NSString *)endpointUrl {
return @"posts/:identifier";
}
######Swift
override var endpointUrl: String {
return "posts/:identifier"
}
####Slug Mapping
Slug mapping is a powerful feature that allows you to populate a given endpoint with slug values as necessary. For example, look at the endpoint url declared above as posts/:identifier
. This means that if we pass a slug into our endpoints initialization, our url will be filled in with the appropriate values. Let's use the following example:
PostsEndpoint *pe = [PostsEndpoint endpointWithSlug:@{@"identifier" : @"17"}];
[pe getWithCompletion: ... ];
Given the example above, our endpoint posts/:identifier
would be mapped to look like thishttp://someBaseUrl.com/posts/17
.
This feature can be used several different ways. The first, as you see above simply replaces the value declared in the endpointUrl with the value passed in the dictionary.
#####1. Dictionaries
If a dictionary has the key declared as a slug path ein the endpointUrl, the value for that key will be superimposed into the Url. If no slug, or no value is found, that url component will be ommitted. In the above example, our final url would be http://someBaseUrl.com/posts
if the endpiont were passed a nil slug.
#####2. Objects - With Keys
You can also pass an object that has the specified keypath. This means that if our post had a property declared like so:
@interface Post : NSObject
@property (copy, nonatomic) NSString *identifier;
@end
```
Now, if we passed the Post as a slug into our endpoint like this, it would be automatically populated:
```ObjC
Post *post = ...;
PostsEndpoint *ep = [PostsEndpoint endpointWithSlug:post];
[ep getWithCompletion: ... ];
```
If our `post` object declared above has an identifier of `352` then we would be sending a get request to the endpoint `http://someBaseUrl.com/posts/352`
#####3. Multiple Object Types
In some situations, we want to pass a variety of objects to an endpoint and define more specifically how that endpoint should be populated with the slug. For these situations, you can override `valueForSlugPath:withSlug` to define what value should be used to populate the url. Our endpoint might look like this:
```ObjC
@implementation PostsEndpoint
/*
...
*/
- (id)valueForSlugPath:(NSString *)slugPath withSlug:(id)slug {
if ([slug isKindOfClass:[Comment class]]) {
Comment *comment = (Comment *)slug;
return comment.post.identifier;
} else {
return [super valueForSlugPath:slugPath withSlug:slug];
}
}
@end
```
By overriding as demonstrated above, we can pass our endpoint a `Dictionary`, a `Post` object, or a `Comment` object and when we fetch from our Posts endpoint, we'll interact with the appropriate endpoint.
a
######Slug Mapping Nil Check
By default, a value is checked if it is `nil` or `NSNull`. If either of these is true, the path is not mapped. In rare cases, you may need to specify what constitutes as `nil`. For example, sometimes when using an NSInteger, it is 0 but needs to be nil.
```ObjC
@implementation PostsEndpoint
/*
...
*/
- (BOOL)valueIsValid:(id)value forSlugPath:(NSString *)slugPath {
if ([slugPath isEqualToString:@"identifier"]) {
return [value intValue] > 0;
} else {
return YES;
}
}
@end
```
#####Response Key Path
If you wish to use a portion of the response located at a specified keypath, you can declare it here. This is only necessary for specific situations. For example, if our response looked like this:
```
[
"results" : [
// ... results
]
]
```
We could specify to map only the array located at `results` by declaring like so:
######ObjC
```ObjC
- (NSString *)responseKeyPath {
return @"results";
}
```
######Swift
```Swift
override var responseKeyPath: String {
return "results"
}
```
####Serializers
In rare situations, you may need to provide a request or response serializer yourself. In those situations use the following:
#####Response Serializer
```ObjC
- (AFHTTPResponseSerializer<AFURLResponseSerialization> *)responseSerializer {
return ...;
}
```
#####Request Serializer
```ObjC
- (AFHTTPRequestSerializer<AFURLRequestSerialization> *)requestSerializer {
return ...;
}
```
####Append Header
In some situations, the header contains valueable data we want to include in mapping. A common example of this is when next / last urls are included for paging in the header.
If the response is a dictionary, an additional field will be added called 'Header', and values can be accessed via keypath syntax, ie: `Header.etag`.
```
[
"Header" : [
"headerKey" : "headerVal",
// ...
]
"responseKey" : "response Val"
"responseKey2": "response val"
]
```
If the response is an array, it will be appended to the key `"response"` for mapping.
```
[
"Header" : [
"headerKey" : "headerVal"
]
"response" : [
// ... array response
]
```
######ObjC
```ObjC
- (BOOL)shouldAppendHeaderToResponse {
return YES;
}
```
######Swift
```Swift
override var shouldAppendHeaderToResponse: Bool {
return true
}
```
####Transform Response
For some apis, the data we receive isn't able to be parsed a valid json representation for mapping. This is most common with XML webservices. In those situations, you can override `transformResponseDataToMappableRawType:`. This can also be overridden for customize behavior of specialized circumstances.
######ObjC
```ObjC
- (id<GenomeMappableRawType>)transformResponseToMappableRawType:(id)response {
if ([response isKindOfClass:[NSData class]]) {
NSData *responseData = response;
NSDictionary *responseDictionary = ... convert response data;
return responseDictionary;
} else {
return response;
}
}
```
######Swit
```Swift
override func transformResponseToMappableRawType(response: AnyObject) -> GenomeMappableRawType? {
if let data = response as? NSData {
return ... converted data
} else {
return response as? GenomeMappableRawType
}
}
```
####Networking Examples
Once you've modeled your endpoint, the majority of the work is done! You simply intialize your endpoint with a slug and parameters as necessary and you're on your way!
#####Get
######Get specific post
```ObjC
PostsEndpoint *ep = [PostsEndpoint endpointWithSlug:@{@"identifier" : @"3"}];
[ep getWithCompletion:^(Post *post, NSError *error){
// ...
}];
```
######Get a user's posts
```ObjC
PostsEndpoint *ep = [PostsEndpoint endpointWithParameters:@{@"user_id" : currentUser.identifier}];
[ep getWithCompletion:^(NSArray *posts, NSError *error){
// ... array of Post objects
}];
```
#####Post
```ObjC
NSDictionary *newPost = @{
@"title" : @"New Post",
@"body" : @"This is a cool new post"
};
PostsEndpoint *ep = [PostsEndpoint endpointWithParameters:newPost];
[ep postWithCompletion:^(Post *post, NSError *error){
// ... created new post, or error
}];
```
#####Put
```ObjC
NSArray *tags = [
@"red",
@"fun",
@"summer"
]
PostTagEndpoint *ep = [PostTagEndpoint endpointWithSlug:post
andParameters:tags];
[ep putWithCompletion:^(NSArray *tags, NSError *error){
// ... created or updated tags for post.
}];
```
#####Patch
```ObjC
NSDictionary *updatedParams = @{
@"title" : @"New Title",
@"body" : @"Updated body"
};
PostsEndpoint *ep = [PostsEndpoint endpointWithSlug:post
andParameters:updatedParams];
[ep patchWithCompletion:^(Post *post, NSError *error){
// ... created new post, or error
}];
```
#####Delete
```ObjC
PostEndpoint *ep = [PostEndpoint endpointWithSlug:post];
[ep deleteWithCompletion:^(Post *deletedPost, NSError *error) {
// .. deleted object or error
}]
```
####Mapping
For more information, see <a href="https://github.com/loganwright/genome">Genome</a>