Polymer 0.1.5

Polymer 0.1.5

测试已测试
Lang语言 Obj-CObjective C
许可 MIT
并发最新发布2015年4月

Logan Wright 维护。



 
依赖
AFNetworking~> 2.0
Genome= 0.1
 

Polymer 0.1.5

Polymer是一个面向端点的网络库,用于Objective-C和Swift,旨在让与REST Web服务交互变得简单、快速和有趣!通过将Web服务的端点视为对象,它使交互更加直观和可读,同时利用和简化我们在消费API时所习惯的一些令人惊叹的映射技术。

该库的目标是实现尽可能最小的体积,同时提供最大的自定义。这是通过创建透明的方法来实现的,当需要处理边缘情况并自定义端点的行为时,可以轻松覆盖这些方法。

Genome

Polymer具有映射库Genome

AFNetworking

Polymer依赖于AFNetworking来执行其网络操作



文档
初步设置
入门指南 -- Spotify搜索
      模型
      端点
      使用
端点
      基础端点
            基本URL
            头部字段
            可接受的内容类型
      单个端点
            返回类
            端点URL
                  别名映射
            响应键路径
            序列化器
                  响应序列化器
                  请求序列化器
            添加头部
      转换响应
网络 - 示例
      GET
      POST
      PUT
      PATCH
      DELETE


初步设置

如果您希望手动安装库,还需要包括AFNetworkingGenome

强烈建议您通过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的艺术家对象。我们稍后会使用此键路径。首先,让我们分离出用于映射的艺术家对象,它看起来像这样

SpotifyArtist JSON表示
{
  "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对象的结构是什么样的。

Spotify模型

SpotifyArtist模型

我们需要做的第一件事是创建我们的对象,并确保它符合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

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的端点。

Spotify端点

我更喜欢在单个文件中声明端点,因为它防止了在添加端点时需要添加额外的导入,而很多端点都变成了相互依赖的。

在你的端点文件中,导入

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种方法。 baseUrlendpointUrlreturnClass。在上面的 SpotifySearchEndpoint 中,你会发现 baseUrl 没有被重写。这是因为它从 SpotifyBaseEndpoint 继承而来,重写了 baseUrl。所有未来的子类都可以继承这个基类。

baseUrl - API 的基本 URL。端点将附加到的地方。

endpointUrl - 端点的 URL。您可以通过前缀冒号 : 来声明更高级别的端点。这些可以从对象智能映射到生成端点。(关于 slug 映射的更多内容将在稍后介绍)。

responseKeyPath - 正如我们最初指定的,这是一个简单的示例,我们不需要响应中的全部信息。我们只想获取位于键路径 artists.items 上的艺术家数组。通过在端点中声明此信息,我们正在告诉它。从 URL 端点 search 中获取项目,然后从响应中获取键路径 artists.items 上的对象。然后映射该响应中的对象到类型 SpotifyArtist

就是这样,我们准备好了,可以使用搜索 API 了!

使用!

一切都已设置,让我们从服务器获取一些对象!Spotify 搜索端点至少需要两个参数,query : q 和 type artistalbumtrack。在我们的示例中,我们正在查询艺术家,所以我们将 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 应附加到的基 URL。在 API 的基类中重写此设置是一种常见的做法,然后进一步子类化端点(见入门指南)。

_objc
- (NSString *)baseUrl {
  return @"http://api.somewebservice.com";
}
Swift
override var baseUrl: String {
  return "http://api.somewebservice.com"
}
头部字段

您可以在其中声明制作 Web 请求到 API 所必需的头部字段。此用例最常见的是接受类型和令牌。同样,通常在特定 API 的基本端点中存在此设置,但根据需要可以为其特定端点覆盖。

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
}
可接受的内容类型

您可以使用此功能来指定来自web服务的被接受的内容类型。同样,这通常在基类中指定,但在必要时也可以由单个端点覆盖。

注意:对于现代的Web服务,通常没有必要覆盖此功能。

Objective-C
- (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"])
}

单个端点

一旦定义了您的基端点,应该创建其子类来指定单个行为。请记住,对于特定的场景,上述每种方法也可在您的端点模型中子类化。

返回类

使用此功能来定义此端点的响应应映射到的模型。如果此端点是数组响应,则端点将返回此类的数组。

注意:此类必须遵循GenomeObject协议

Objective-C
- (Class)returnClass {
  return [Post class];
}
Swift
override var returnClass: AnyClass {
    return Post.self
}
端点URL

在这里声明在基本URL上附加端点。您还可以在此处指定用于填充URL的slug路径。常见实现示例如下

Objective-C
- (NSString *)endpointUrl {
  return @"posts/:identifier";
}
Swift
override var endpointUrl: String {
    return "posts/:identifier"
}

Slug映射

Slug映射是一个强大的功能,允许您根据需要将slug值填充到给定的端点。例如,查看上述声明为posts/:identifier的端点URL。这意味着如果我们向端点初始化传递一个slug,我们的URL将填充适当的值。以下是一个示例

PostsEndpoint *pe = [PostsEndpoint endpointWithSlug:@{@"identifier" : @"17"}];
[pe getWithCompletion: ... ];

根据上述示例,我们的端点posts/:identifier将被映射为如下所示http://someBaseUrl.com/posts/17

此功能可以使用多种不同的方法。第一种,如上所示,简单地将声明的端点URL中的值替换为字典中传递的值。

1. 字典

如果字典具有在端点URL中声明的slug路径键,则该键的值将叠加到URL中。如果没有slug或没有找到值,将省略该URL组件。在上面的示例中,如果向端点传递一个nil slug,则我们的最终URL将是http://someBaseUrl.com/posts

2. 对象 - 使用键

您还可以传递具有指定键路径的对象。这意味着,如果我们有如下所述声明的一个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请求

3. 多种对象类型

在某些情况下,我们希望将各种对象传递到端点,并更具体地定义端点应该如何使用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

如上所示,通过重写,我们可以向端点传递DictionaryPost对象或Comment对象,当我们从Post端点获取数据时,我们会与适当的端点进行交互。

Slug映射空值检查

默认情况下,会检查值是否是nilNSNull。如果这两个中的任何一个为真,路径不会进行映射。在极少数情况下,您可能需要指定什么构成nil。例如,有时当使用时,它是0,但需要为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的数组,如下所示声明:

ObjC
- (NSString *)responseKeyPath {
  return @"results";
}
Swift
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
  ]
ObjC
- (BOOL)shouldAppendHeaderToResponse {
  return YES;
}
Swift
override var shouldAppendHeaderToResponse: Bool {
  return true
}

转换响应

对于一些API,我们接收到的数据无法解析为有效的JSON表示形式,以便进行映射。这种情况在XML Web服务中最常见。在这种情况下,您可以重写transformResponseDataToMappableRawType:。这也可以重写以自定义特殊情况的特定行为。

ObjC
- (id<GenomeMappableRawType>)transformResponseToMappableRawType:(id)response {
  if ([response isKindOfClass:[NSData class]]) {
        NSData *responseData = response;
        NSDictionary *responseDictionary = ... convert response data;
        return responseDictionary;
  } else {
    return response;
  }
}
Swift
override func transformResponseToMappableRawType(response: AnyObject) -> GenomeMappableRawType? {
  if let data = response as? NSData {
    return ... converted data
  } else {
    return response as? GenomeMappableRawType
  }
}

网络示例

一旦您已建模端点,大多数工作就已经完成!您只需使用slug以及必要的参数初始化端点,然后就可以上路了!

GET请求
获取特定文章
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