PEHateoas-Client 1.0.18

PEHateoas-Client 1.0.18

测试已测试
语言语言 Obj-CObjective C
许可证 MIT
发布最后发布2016年7月

Paul Evans 维护。



 
依赖项
AFNetworking~> 2.6.3
PEObjc-Commons~> 1.0.111
CocoaLumberjack~> 1.9
 

  • 作者
  • Paul Evans

PEHateoas-Client 是一个用于简化消耗超媒体 REST API 的 iOS 静态库。PEHateoas-Client 是基于 AFNetworking 构建的。目前 PEHateoas-Client 支持了 HAL(我们忽略了 CURIEs,并稍微修改了嵌入资源的概念)的变体和子集。

PEHateoas-Client 是 PE* iOS 库套件的一部分。

目录

动机

简单来说,为了在 iOS 应用程序中轻松消费超媒体 REST API。名称 PEHateoas-Client 来自于缩写 HATEOAS:**超媒体作为应用程序状态引擎**,这是一种针对 RESTful 网络服务设计的方法,其中资源的 URL 端点不是事先已知的;相反,客户端知道起始 URL 以及 Web 服务支持的链接关系集和媒体类型。

HATEOAS 资源

设计

主要抽象

以下是 PEHateoas-Client 的主要抽象,以 Objective-C 类的形式呈现。

HCMediaType

表示一个 互联网媒体类型。每个 HTTP 资源都有一个与之相关的媒体类型。

HCResource

代表HTTP资源。资源定义为一种类型化的内容(具有媒体类型),并拥有一个URI。资源还可以包含一组嵌入式链接。每个链接代表封装资源与某个目标资源之间的关系。

HCRelation

代表超媒体链接关系。(*注意:“关系”在此处并不与数学中的“关系”具有相同的意义。将超媒体链接关系视为一个更加强有力的“关系”可能更为恰当。*)超媒体链接关系本质上是二元的。除了具有一个名称,一个关系还具有源资源和目标资源。在HAL格式的上下文中,关系以一个对象的形式存在于_links JSON条目内部。一个_links JSON对象是一个JSON对象的集合;_links对象中的每个键都是链接关系的名称。例如,给定以下JSON

{ "fuelstation-name": "7-Eleven",
  "price-per-gallon": 2.89,
  "car-wash-pergallon-discount": 0.15,
  "_links": {
    "self": {
        "href": "https://fp.example.com/fuelstations/fs291410",
        "type": "application/vnd.fuelstation.example.com-v1.0+json"
    },
    "purchase_logs": {
        "href": "https://fp.example.com/fuelstations/fs291410/purchase-logs",
        "type": "application/vnd.fplog.example.com-v1.0+json"}}}

我们有一个包含2个超媒体链接的加油站资源。这些链接的关系名称是:selfpurchase_logs。self关系的名称为"self";其源资源和目标资源是封装资源(因此是自引用的)。上述示例资源的URI是Self链接的href属性的值。purchase_logs关系的名称为"purchase_logs";其源资源是封装资源,其目标资源的URI为https://fp.example.com/fuelstations/fs291410/purchase-logs,类型为application/vnd.fuelstation.example.com-v1.0+json

HCRelationExecutor

用于导航到链接关系的目标资源的手段。通常在遇到资源并检查其嵌入式链接关系集合时,我们希望导航到这些链接的目标资源。在这里,导航是一个广义的概念。它可能意味着以下任何标准的HTTP操作

  • POST、GET、PUT、DELETE等。

HCRelationExecutor的一个实例用于导航到链接关系的目标资源。HCRelationExecutor公开以下函数

  • doPostForTargetResource: ...
  • doGetForTargetResource: ...
  • doPutForTargetResource: ...
  • doDeleteForTargetResource: ...

这些函数接受一个相当大的参数集。查看API文档获取详细信息。

HCAuthentication和HCAuthorization

在发出HTTP请求并返回401(未经授权)时,HCAuthReqdErrorBlk块(下面描述)会接收到一个HCAuthentication实例。认证实例包含解析后的WWW-Authenticate响应头信息(即方案和域)。

可以将HCAuthorization实例作为参数提供给HAL格式HCRelationExecutor中的每个doXXXTargetResource: ...方法。如果提供,将在HTTP请求中包含Authorization头。授权实例通常封装三个部分:方案、参数和值。存在一个工厂函数,可以简化单个参数/值的HCAuthorization实例的创建。

HCRelationExecutor函数的Block类型

HCGETSuccessBlk

doGetForTargetResource:...的完成/成功块。在这个块中,您将免费获得来自HTTP响应的大部分重要信息,包括

  • 获取的资源的位置(NSURL*)
  • 模型对象(使用您提供的序列化器从响应体中解析得到)
  • 嵌入在资源体内的链接关系集合(作为NSDictionary*)
  • 获取资源的最后修改日期(为NSDate*类型)
  • 原始的HTTP响应(NSHTTPURLResponse*类型)(如果需要的话)

与其它关系执行函数相关的完成块类型提供相同的通用参数。块类型包括:HCPOSTSuccessBlkHCPUTSuccessBlkHCDELETESuccessBlk等。

除了支持HATEOAS,PEHateoas-Client在总体上是一层甜美的顶层封装,基于AFNetworking。AFNetworking为其函数提供2种基本的完成块类型,用于成功和失败。而PEHateoas-Client提供完成块用于以下情况:

  • 成功块(针对任何2XX的响应代码)
  • 认证要求块(针对401的响应代码) - 包括HCAuthentiation参数,封装了“WWW-Authenticate”头信息的解析部分,即方案和领域部分
  • 重定向块(除了301/302/303之外的3XX响应代码) - 对于301、302、303,AFNetworking将自动跟随重定向链接
  • 冲突块(针对409的响应)
  • 客户端错误块(针对所有其他4XX响应代码)
  • 服务器不可用块(针对503的响应) - 包括作为retry-after日期的NSDate参数(如果存在“Retry-After”头信息)
  • 服务器错误块(针对所有其他5XX响应代码)

如你所见,PEHateoas-Client对HTTP响应做了一些基本解析,并按照提供的块来调用。每个块类型都会接收到原始的NSHTTPURLResponse,以供进一步遍历。

序列化器

PEHateoas-Client允许您配置序列化器,用于将模型对象序列化并包含到HTTP请求体中(用于POST和PUT请求),以及将响应体(如果存在)反序列化为模型对象。

开箱即用的,一个具体的序列化器HCHalJsonSerializer提供了针对部分基于JSON的HAL格式(《JSON-based HAL format”)的子集(CURIE被忽略)。需要注意的是,即使资源的媒体类型不是application/hal+json,此序列化器也可以使用。

当使用HCHalJsonSerializer序列化器时,《HCGETSuccessBlk》块的模型参数将简单地是一个经过解析的JSON体的NSDictionary(已省略_links条目)。您可以通过提供自己的自定义序列化器来覆盖此行为。如果您在没有收到成功块的模型对象参数(而是dsn您的自定义模型对象时希望使用),则可以子类化HCHalJsonSerializerExtensionSupport。您必须重写2个方法:

  • dictionaryWithResourceModel: - 这是为了将您的模型对象序列化为NSDictionary(这将被进一步转换为JSON),
  • resourceModelWithDictionary:relations:mediaType:location:lastModified: - 这是为了反序列化一个NSDictionary(它来自JSON响应体,省略了_links条目)到您的模型对象(在进行此反序列化时,您还将收到已解析的链接关系、媒体类型、位置以及最后修改日期 - 如果存在,当然)。

嵌入资源

HAL格式允许资源嵌套在其他资源中(使用<_embedded>键)。与HAL格式相比,PEHateoas-Client对嵌入资源的看法略有不同。在加油站资源的基础上,下例展现了PEHateoas-Client理解的嵌入示例:

{ "fuelstation-name": "7-Eleven",
  "price-per-gallon": 2.89,
  "car-wash-pergallon-discount": 0.15,
  "_links": {
    "self": {
        "href": "https://fp.example.com/fuelstations/fs291410",
        "type": "application/vnd.fuelstation.example.com-v1.0+json"
    },
    "purchase_logs": {
        "href": "https://fp.example.com/fuelstations/fs291410/purchase-logs",
        "type": "application/vnd.fplog.example.com-v1.0+json"}},
  "_embedded": [
    {"media-type": "application/vnd.fplog.example.com-v1.0+json",
     "location": "",
     "last-modified": "",
     "paylaod": {
       "log-date": "",
       "num-gallons-purchased": 14.9,
       "odometer-reading": 52981}},
    ...
    {"media-type": "application/vnd.envlog.example.com-v1.0+json",
     "location": "",
     "last-modified": "",
     "paylaod": {
       "log-date": "",
       "outside-temperature": 72,
       "atmospheric-pressure": 101325}},
    ...
  ]}

在我们的示例中,_embedded条目的值是一个对象数组(这与HAL相反,其中_embedded条目的值将是另一个对象)。数组似乎更适合用作_embedded,因此我们选择了它。此外,数组中的每个嵌入对象都有3个有用的元数据:media-typelocationlast-modifiedpayload条目包含嵌入资源的实际内容。

为了让序列化器能够处理嵌入资源,在构造它时,只需向HCResourceSerializerSupport的初始化器(HCHalJsonSerializerHCResourceSerializerSupport扩展)的serializersForEmbeddedResourcesactionsForEmbeddedResources部分提供一个适当的字典。让我们看看一个示例。假设你有以下模型类

@interface FPFuelPurchaseLog : NSObject
@property (nonatomic, readonly) NSDate *logDate;
@property (nonatomic, readonly) NSDecimalNumber *odometerReading;
@property (nonatomic, readonly) NSDecimalNumber *numGallonsPurchased;
@end

@interface FPFuelStation : NSObject
@property (nonatomic, readonly) NSString *fuelStationName;
@property (nonatomic, readonly) NSDecimalNumber *pricePerGallon;
@property (nonatomic, readonly) NSArray *fpLogs;

- (void)addFpLog:(FPFuelPurchaseLog *)fpLog;
@end

并且你有2个序列化器类(为每个模型类)。每个序列化器类仅关注其对应模型的直接非集合属性。即,加油站序列化器不会关注燃料购买记录实例(即使它引用了它们的一个NSArray)。

@interface FPFuelPurchaseLogSerializer : HCHalJsonSerializerExtensionSupport
@end

@interface FPFuelStationSerializer : HCHalJsonSerializerExtensionSupport
@end

现在来看看利用序列化器的代码。

// define our serializers
HCMediaType *fpLogMediaType = [HCMediaType mediaTypeFromString:@"application/vnd.fp.example.com-v1.0+json"];
FPFuelPurchaseLogSerializer *fpLogSerializer =
  [[FPFuelPurchaseLogSerializer alloc] initWithMediaType:fpLogMediaType
                                                 charset:[HCCharset UTF8]
                         serializersForEmbeddedResources:@{} // fplog resources will NOT have embedded resources
                             actionsForEmbeddedResources:@{}];

HCMediaType *fuelStationMediaType = [HCMediaType mediaTypeFromString:@"application/vnd.fp.example.com-v1.0+json"];
HCActionForEmbeddedResource actionForEmbeddedFpLog = ^(id fuelStation, id embeddedFpLog) {
  [(FPFuelStation *)fuelStation addFpLog:embeddedFpLog];
};
FPFuelStationSerializer *fuelStationSerializer =
  [[FPFuelStationSerializer alloc] initWithMediaType:fuelStationMediaType
                                             charset:[HCCharset UTF8]
                     serializersForEmbeddedResources:@{[fpLogMediaType description] : fpLogSerializer}
                         actionsForEmbeddedResources:@{[fpLogMediaType description] :
                         actionForEmbeddedFpLog}];

// deserialize our fuel station JSON
NSString *fuelStationJsonAsStr = ...; // assume fuelStationJsonAsStr now holds our fuel station JSON defined above
NSDictionary *fuelStationJsonAsDict =
  [NSJSONSerialization JSONObjectWithData:[fuelStationJsonAsStr dataUsingEncoding:NSUTF8StringEncoding]
                                  options:0
                                    error:nil];
HCDeserializedPair *pair = [fuelStationSerializer deserializeEmbeddedResource:fuelStationJsonAsDict];
FPFuelStation *fuelStation = [pair resourceModel];
NSDictionary *fuelStationRels = [pair relations];

我们的fuelStationSerializer被初始化,以便如果它在解析HAL JSON时遇到_embedded条目,对于与fpLogMediaType封装的媒体类型匹配的每个嵌入资源,将使用fpLogSerializer来反序列化它,并将结果作为我们的actionForEmbeddedFpLog块的第二个参数提供。正如你所看到的,我们的actionForEmbeddedFpLog块的实现是将给定的embeddedFpLog实例添加到fuelStation的集合中。如果不是显而易见的,actionForEmbeddedFpLog块的fuelStation参数将是当前由fuelStationSerializer解析的加油站实例。

需要注意的是,当使用PEHateoas-Client时,您永远不必手动调用序列化器。实际上,HCResourceSerializer中定义了一个解序列化方法,它接受NSHTTPURLResponse(以及其他一些内容)并执行解序列化;但是,您不必在正常应用程序代码中调用它。相反,它是由您的HCRelationExecutor实例执行的。

示例使用

对于以下示例,假设我们有上面的加油站序列化器。还假设我们掌握了加油站的所有权。(我们如何知道加油站的URI是一个单独的话题。只是假设我们从先前的GET请求中得到了它。记住,REST客户机具有*起点** URI的知识;因此,假设从起点开始,客户端能够通过正常的REST/超媒体遍历获得加油站的所有权。)

获取

HCRelationExecutor *relExec =
  [[HCRelationExecutor alloc] initWithDefaultAcceptCharset:[HCCharset UTF8]
                                     defaultAcceptLanguage:@"en-US"
                                 defaultContentTypeCharset:[HCCharset UTF8]
                                  allowInvalidCertificates:NO];
NSURL *fuelStationUrl = [[NSURL alloc]initWithString:@"/fuelstations/fs291410"
                                       relativeToURL:[NSURL URLWithString:@"https://fp.example.com"]];
HCResource *fuelStationRes = [[HCResource alloc] initWithMediaType:fuelStationMediaType
                                                               uri:fuelStationUrl];
__block FPFuelStation *fetchedFuelStation;
__block NSDictionary *fuelStationRelations;
HCGETSuccessBlk successBlk = ^(NSURL *location,
                               id resourceModel,
                               NSDate *lastModified,
                               NSDictionary *relations,
                               NSHTTPURLResponse *resp) {
  fetchedFuelStation = (FPFuelStation *)resourceModel;
  fuelStationRelations = relations;
  NSLog(@"Got the fuel station and its relations!");
};
HCRedirectionBlk redirectionBlk = ^(NSURL *location,
                                    BOOL movedPermanently,
                                    BOOL notModified,
                                    NSHTTPURLResponse *resp) {
  NSLog(@"The target resource is somewhere else!");
};
HCClientErrorBlk clientErrBlk = ^(NSHTTPURLResponse *resp) {
  NSLog(@"Client error!");
};
HCAuthReqdErrorBlk authRequiredBlk = ^(HCAuthentication *auth,
                                       NSHTTPURLResponse *resp) {
  NSLog(@"Authentication required!");
};
HCServerErrorBlk serverErrBlk = ^(NSHTTPURLResponse *resp) {
  NSLog(@"Server error!");
};
HCServerUnavailableBlk serverUnavailableBlk = ^(NSDate *retryAfter,
                                                NSHTTPURLResponse *resp) {
  NSLog(@"The server is currently unavailable!");
};
HCConnFailureBlk connFailureBlk = ^(NSInteger nsurlErr) {
  NSLog(@"Connection error!");
};
[relExec doGetForTargetResource:fuelStationRes
                ifModifiedSince:nil
               targetSerializer:fuelStationSerializer
                   asynchronous:YES // YES to put AFNetworking request op onto operationQueue; NO to directly start the operation
                completionQueue:nil // run completion block on main thread
                  authorization:nil // don't supply an 'Authorization' request header
                        success:successBlk
                    redirection:redirectionBlk
                    clientError:clientErrBlk
         authenticationRequired:authRequiredBlk
                    serverError:serverErrBlk
               unavailableError:serverUnavailableBlk
              connectionFailure:connFailureBlk
                        timeout:60
                    cachePolicy:NSURLRequestUseProtocolCachePolicy
                   otherHeaders:@{}];

论坛

假设在过去的某个时候,我们得到了对某个HTTP请求的401状态码,而那401的WWW-Authenticate响应头看起来是这样的:WWW-Authenticate: FPAuthToken realm='all'在我们的应用中,我们将提示用户登录,收集他们的凭据,导航到某个认证关系的目标资源,并获得一个认证令牌:FPAUTHTKN-0291034K-ASNLUS-ZDLSI920。我们想将这个令牌包含在我们的帖子示例中。

在适用的情况下,我们将在我们的GET示例中重用我们定义的一些完成块。我们还将使用我们在HCGETSuccessBlk中获取的fuelStationRelations链接集合。以下显示了针对我们获取的加油站创建新的燃料购买记录的 posters。

HCRelation *purchaseLogsRel = [fuelStationRelations objectForKey:@"purchase_logs"];
FPFuelPurchaseLog *fpLog = [FPFuelPurchaseLog makeWithLogDate:[NSDate date]
                                              odometerReading:@(25004.2)
                                          numGallonsPurchased:@(14.7)];
HCPOSTSuccessBlk successBlk = ^(NSURL *location,
                                id resourceModel,
                                NSDate *lastModified,
                                NSDictionary *relations,
                                NSHTTPURLResponse *resp) {
  if (resourceModel) {
    FPFuelPurchaseLog *newFpLog = (FPFuelPurchaseLog *)resourceModel; // if the created fpLog is echoed back in the response
    NSLog(@"Our newly-minted fpLog instance! %@", newFpLog);
  }
};
HCAuthorization *authorization = [HCAuthorization authWithScheme:@"fp-auth"
                                             singleAuthParamName:@"fp-token"
                                                  authParamValue:@"FPAUTHTKN-0291034K-ASNLUS-ZDLSI920"];
[relExec doPostForTargetResource:[purchaseLogsRel target
              resourceModelParam:fpLog // the model object to be serialized and become the body of the request
                 paramSerializer:fpLogSerializer // serializes the resourceModelParam to be the body of the request
        responseEntitySerializer:fpLogSerializer // if present, deserializes the response into a model object
                    asynchronous:YES // to put AFNetworking request op onto operationQueue; NO to directly start the operation
                 completionQueue:nil // run completion block on main thread
                   authorization:authorization
                         success:successBlk
                     redirection:redirectionBlk
                     clientError:clientErrBlk
          authenticationRequired:authRequiredBlk
                     serverError:serverErrBlk
                unavailableError:serverUnavailableBlk
               connectionFailure:connFailureBlk
                         timeout:60
                    otherHeaders:@{}];

PE* iOS库套件

(每个库都实现为CocoaPod启用的iOS静态库)。