测试已测试 | ✗ |
Lang语言 | Obj-CObjective C |
许可 | MIT |
并发最新发布 | 2015年4月 |
由 Logan Wright 维护。
依赖 | |
AFNetworking | ~> 2.0 |
Genome | = 0.1 |
Polymer是一个面向端点的网络库,用于Objective-C和Swift,旨在让与REST Web服务交互变得简单、快速和有趣!通过将Web服务的端点视为对象,它使交互更加直观和可读,同时利用和简化我们在消费API时所习惯的一些令人惊叹的映射技术。
该库的目标是实现尽可能最小的体积,同时提供最大的自定义。这是通过创建透明的方法来实现的,当需要处理边缘情况并自定义端点的行为时,可以轻松覆盖这些方法。
Polymer具有映射库Genome
Polymer依赖于AFNetworking来执行其网络操作
文档
初步设置
入门指南 -- Spotify搜索
模型
端点
使用
端点
基础端点
基本URL
头部字段
可接受的内容类型
单个端点
返回类
端点URL
别名映射
响应键路径
序列化器
响应序列化器
请求序列化器
添加头部
转换响应
网络 - 示例
GET
POST
PUT
PATCH
DELETE
如果您希望手动安装库,还需要包括AFNetworking和Genome
强烈建议您通过cocoapods安装Polymer。这里有一个个人cocoapods参考,以防万一有用:Cocoapods设置指南
Podfile: pod 'Polymer'
导入: #import <Polymer/PLYEndpoint.h>
描述Polymer的最佳方式是展示其用法。让我们查询一些来自Spotify Web API的艺术家。以下是端点此处的示例响应。
{
"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
}
}
对于我们的示例,我们真正关心的是位于键路径artists.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"
}
让我们看看它作为Objective-C对象的结构是什么样的。
我们需要做的第一件事是创建我们的对象,并确保它符合GenomeMappableObject
协议。现在我们的对象看起来是这样的。
SpotifyArtist.h
#import <Foundation/Foundation.h>
#import <Genome/Genome.h>
@interface SpotifyArtist : NSObject <GenomeMappableObject>
@end
现在让我们填充与JSON映射的属性。我们的最终模型头将看起来像这样
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
协议要求实现一个实例方法,该方法名为mapping
,并返回一个NSMutableDictionary
。这在将JSON响应转换为模型对象时会私下使用。建模支持以下语法
mapping[@"<#propertyName#>"] = @"<#associatedJsonKeyPath#>";
这个操作试图变得智能,如果你有一个属性也是一个对应于GenomeObject
的类的属性,它将会自动映射。如果你的属性是GenomeObject
数组的,类型必须显式声明,因为这无法通过反射发现。为此,你使用以下语法
mapping[@"<#arrayPropertyName#>@<#ClassName#>"] = @"<#associatedJsonKeyPath#>";
@
语法是Genome的一个重要功能,它将被相当频繁地使用。如果你想更加类型安全,你可以使用这个便利函数来声明你的键
propertyMap(@"<#propertyName#>", [<#classType#> class])
这是一种映射的安全性更高的方法,这样,如果你对你的类名进行重构,你不需要在整个项目中搜索并替换你的键映射。
这种语法也可以用来声明一个与类一起使用的GenomeTransformer
。关于这一点将在稍后讨论。让我们看一下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
如上所示,我们的images
属性是一个数组,并且我们正在将它的内容映射到我们尚未创建的SpotifyImageRef
模型。让我们看一下位于images
键中的json
"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
} ]
现在,让我们创建一个看起来像这样的单个对象模型
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
注意:你可以如果你喜欢,在实现文件中声明GenomeObject
协议。这对示例来说更清晰。
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
这是一个非常简单的对象,我们的属性名称直接对应于JSON。到目前为止,仍然有必要在映射中声明这些属性。这样做是为了允许对该操作有绝对的控制。
以上就完成了,我们的模型都已经设置好了,现在我们需要设置Spotify API的端点。
我更喜欢在单个文件中声明端点,因为它防止了在添加端点时需要添加额外的导入,而很多端点都变成了相互依赖的。
在你的端点文件中,导入
SpotifyEndpoints.h
#import <Foundation/Foundation.h>
#import <Polymer/PLYEndpoint.h>
我将要做的是声明一个基本端点。这样做是为了提供基础URL以及任何你想要为给定API配置的请求配置。
#import <Foundation/Foundation.h>
#import <Polymer/PLYEndpoint.h>
@interface SpotifyBaseEndpoint : PLYEndpoint
@end
现在让我们看看实现
SpotifyEndpoints.m
#import "SpotifyEndpoints.h"
#import "SpotifyArtist.h"
@implementation SpotifyBaseEndpoint
- (NSString *)baseUrl {
return @"https://api.spotify.com/v1";
}
@end
Spotify是一个现代且干净的API,大多数特性都可以很容易地推断出来,如果你想对你的基本端点有更多的控制,你可以通过添加更多方法重写创建更复杂的实现。一个更具体的API可能会是这样的
@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
好的,现在回到Spotify上。是时候为我们的搜索添加一个新的端点了。这个端点以及所有将来需要这个基础URL的端点都将成为我们的Spotify基本端点的子类。
这是添加搜索端点后的端点文件看起来是什么样的
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
专为使用实施的端点至少实现3种方法。 baseUrl
、endpointUrl
和 returnClass
。在上面的 SpotifySearchEndpoint
中,你会发现 baseUrl
没有被重写。这是因为它从 SpotifyBaseEndpoint
继承而来,重写了 baseUrl
。所有未来的子类都可以继承这个基类。
baseUrl
- API 的基本 URL。端点将附加到的地方。
endpointUrl
- 端点的 URL。您可以通过前缀冒号 :
来声明更高级别的端点。这些可以从对象智能映射到生成端点。(关于 slug 映射的更多内容将在稍后介绍)。
responseKeyPath
- 正如我们最初指定的,这是一个简单的示例,我们不需要响应中的全部信息。我们只想获取位于键路径 artists.items
上的艺术家数组。通过在端点中声明此信息,我们正在告诉它。从 URL 端点 search
中获取项目,然后从响应中获取键路径 artists.items
上的对象。然后映射该响应中的对象到类型 SpotifyArtist
。
就是这样,我们准备好了,可以使用搜索 API 了!
一切都已设置,让我们从服务器获取一些对象!Spotify 搜索端点至少需要两个参数,query : q
和 type artist
、album
或 track
。在我们的示例中,我们正在查询艺术家,所以我们将 type 设置为 artist。现在我们初始化端点并调用 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);
}];
因为 Objective-C 允许在类型转换上的灵活性,所以我们可以在上面的示例中跳过类型转换,并将我们的对象显式替换为其类型。
PLYEndpoint *ep = [SpotifySearchEndpoint endpointWithParameters:@{@"q" : @"beyonce", @"type" : @"artist"}];
[ep getWithCompletion:^(NSArray *artists, NSError *error) {
NSLog(@"Got artists: %@ w/ error: %@", artists, error);
}];
这也可以应用于单个模型对象,而不仅仅是 NSArray
。有关更多信息,请参阅详细的头部字段文档!
将您的 API 端点视为一个对象,这个类就是其模型。它具有以下组件
它从一组基础属性开始,这些属性旨在在端点子类中被覆盖。
在使用 Web 服务时,通常有一组适用于所有端点的基本配置。这些覆盖通常在基类中声明,然后被端点继承;然而,根据需要,这些配置总可以通过单个端点被覆盖。
这表示端点 URL 应附加到的基 URL。在 API 的基类中重写此设置是一种常见的做法,然后进一步子类化端点(见入门指南)。
- (NSString *)baseUrl {
return @"http://api.somewebservice.com";
}
override var baseUrl: String {
return "http://api.somewebservice.com"
}
您可以在其中声明制作 Web 请求到 API 所必需的头部字段。此用例最常见的是接受类型和令牌。同样,通常在特定 API 的基本端点中存在此设置,但根据需要可以为其特定端点覆盖。
- (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;
}
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
}
您可以使用此功能来指定来自web服务的被接受的内容类型。同样,这通常在基类中指定,但在必要时也可以由单个端点覆盖。
注意:对于现代的Web服务,通常没有必要覆盖此功能。
- (NSSet *)acceptableContentTypes {
return [NSSet setWithObjects:@"application/json", @"text/html", nil];
}
override var acceptableContentTypes: Set<NSObject> {
return Set(["application/json", "text/html"])
}
override var acceptableContentTypes: Set<NSObject> {
return Set(["application/json", "text/html", "text/html; charset=utf-8"])
}
一旦定义了您的基端点,应该创建其子类来指定单个行为。请记住,对于特定的场景,上述每种方法也可在您的端点模型中子类化。
使用此功能来定义此端点的响应应映射到的模型。如果此端点是数组响应,则端点将返回此类的数组。
注意:此类必须遵循GenomeObject协议
- (Class)returnClass {
return [Post class];
}
override var returnClass: AnyClass {
return Post.self
}
在这里声明在基本URL上附加端点。您还可以在此处指定用于填充URL的slug路径。常见实现示例如下
- (NSString *)endpointUrl {
return @"posts/:identifier";
}
override var endpointUrl: String {
return "posts/:identifier"
}
Slug映射是一个强大的功能,允许您根据需要将slug值填充到给定的端点。例如,查看上述声明为posts/:identifier
的端点URL。这意味着如果我们向端点初始化传递一个slug,我们的URL将填充适当的值。以下是一个示例
PostsEndpoint *pe = [PostsEndpoint endpointWithSlug:@{@"identifier" : @"17"}];
[pe getWithCompletion: ... ];
根据上述示例,我们的端点posts/:identifier
将被映射为如下所示http://someBaseUrl.com/posts/17
。
此功能可以使用多种不同的方法。第一种,如上所示,简单地将声明的端点URL中的值替换为字典中传递的值。
如果字典具有在端点URL中声明的slug路径键,则该键的值将叠加到URL中。如果没有slug或没有找到值,将省略该URL组件。在上面的示例中,如果向端点传递一个nil slug,则我们的最终URL将是http://someBaseUrl.com/posts
。
您还可以传递具有指定键路径的对象。这意味着,如果我们有如下所述声明的一个post属性
@interface Post : NSObject
@property (copy, nonatomic) NSString *identifier;
@end
现在,如果我们将Post作为slug传递给端点,它将自动填充
Post *post = ...;
PostsEndpoint *ep = [PostsEndpoint endpointWithSlug:post];
[ep getWithCompletion: ... ];
如果上述post对象具有352
的标识符,那么我们将向端点http://someBaseUrl.com/posts/352
发送GET请求
在某些情况下,我们希望将各种对象传递到端点,并更具体地定义端点应该如何使用slug进行填充。对于这些情况,您可以重写valueForSlugPath:withSlug
以定义用于填充URL的值。我们的端点可能看起来像这样:
@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
如上所示,通过重写,我们可以向端点传递Dictionary
、Post
对象或Comment
对象,当我们从Post端点获取数据时,我们会与适当的端点进行交互。
默认情况下,会检查值是否是nil
或NSNull
。如果这两个中的任何一个为真,路径不会进行映射。在极少数情况下,您可能需要指定什么构成nil
。例如,有时当使用
@implementation PostsEndpoint
/*
...
*/
- (BOOL)valueIsValid:(id)value forSlugPath:(NSString *)slugPath {
if ([slugPath isEqualToString:@"identifier"]) {
return [value intValue] > 0;
} else {
return YES;
}
}
@end
如果您希望使用位于指定键路径的响应部分,您可以在这里声明它。这仅适用于特定情况。例如,如果我们的响应如下所示:
[
"results" : [
// ... results
]
]
我们可以指定仅映射位于results
的数组,如下所示声明:
- (NSString *)responseKeyPath {
return @"results";
}
override var responseKeyPath: String {
return "results"
}
在极少数情况下,您可能需要自己提供请求或响应序列化器。在这种情况下,请使用以下内容:
- (AFHTTPResponseSerializer<AFURLResponseSerialization> *)responseSerializer {
return ...;
}
- (AFHTTPRequestSerializer<AFURLRequestSerialization> *)requestSerializer {
return ...;
}
在有些情况下,头包含了我们希望在映射中包含的有价值数据。一个常见的例子是在头中包含用于分页的next / last urls。
如果响应是一个字典,将添加一个称为'Header'的额外字段,可以通过键路径语法访问值,即:Header.etag
。
[
"Header" : [
"headerKey" : "headerVal",
// ...
]
"responseKey" : "response Val"
"responseKey2": "response val"
]
如果响应是一个数组,它将附加到键"response"
进行映射。
[
"Header" : [
"headerKey" : "headerVal"
]
"response" : [
// ... array response
]
- (BOOL)shouldAppendHeaderToResponse {
return YES;
}
override var shouldAppendHeaderToResponse: Bool {
return true
}
对于一些API,我们接收到的数据无法解析为有效的JSON表示形式,以便进行映射。这种情况在XML Web服务中最常见。在这种情况下,您可以重写transformResponseDataToMappableRawType:
。这也可以重写以自定义特殊情况的特定行为。
- (id<GenomeMappableRawType>)transformResponseToMappableRawType:(id)response {
if ([response isKindOfClass:[NSData class]]) {
NSData *responseData = response;
NSDictionary *responseDictionary = ... convert response data;
return responseDictionary;
} else {
return response;
}
}
override func transformResponseToMappableRawType(response: AnyObject) -> GenomeMappableRawType? {
if let data = response as? NSData {
return ... converted data
} else {
return response as? GenomeMappableRawType
}
}
一旦您已建模端点,大多数工作就已经完成!您只需使用slug以及必要的参数初始化端点,然后就可以上路了!
PostsEndpoint *ep = [PostsEndpoint endpointWithSlug:@{@"identifier" : @"3"}];
[ep getWithCompletion:^(Post *post, NSError *error){
// ...
}];
PostsEndpoint *ep = [PostsEndpoint endpointWithParameters:@{@"user_id" : currentUser.identifier}];
[ep getWithCompletion:^(NSArray *posts, NSError *error){
// ... array of Post objects
}];
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
}];
NSArray *tags = [
@"red",
@"fun",
@"summer"
]
PostTagEndpoint *ep = [PostTagEndpoint endpointWithSlug:post
andParameters:tags];
[ep putWithCompletion:^(NSArray *tags, NSError *error){
// ... created or updated tags for post.
}];
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
}];
PostEndpoint *ep = [PostEndpoint endpointWithSlug:post];
[ep deleteWithCompletion:^(Post *deletedPost, NSError *error) {
// .. deleted object or error
}]
更多信息,请参阅 Genome