测试已测试 | ✓ |
语言语言 | Obj-CObjective C |
许可证 | MIT |
发布最后发布 | 2016年7月 |
由 Paul Evans 维护。
依赖项 | |
AFNetworking | ~> 2.6.3 |
PEObjc-Commons | ~> 1.0.111 |
CocoaLumberjack | ~> 1.9 |
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 服务支持的链接关系集和媒体类型。
以下是 PEHateoas-Client 的主要抽象,以 Objective-C 类的形式呈现。
表示一个 互联网媒体类型。每个 HTTP 资源都有一个与之相关的媒体类型。
代表HTTP资源。资源定义为一种类型化的内容(具有媒体类型),并拥有一个URI。资源还可以包含一组嵌入式链接。每个链接代表封装资源与某个目标资源之间的关系。
代表超媒体链接关系。(*注意:“关系”在此处并不与数学中的“关系”具有相同的意义。将超媒体链接关系视为一个更加强有力的“关系”可能更为恰当。*)超媒体链接关系本质上是二元的。除了具有一个名称,一个关系还具有源资源和目标资源。在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个超媒体链接的加油站资源。这些链接的关系名称是:self和purchase_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
。
用于导航到链接关系的目标资源的手段。通常在遇到资源并检查其嵌入式链接关系集合时,我们希望导航到这些链接的目标资源。在这里,导航是一个广义的概念。它可能意味着以下任何标准的HTTP操作
HCRelationExecutor的一个实例用于导航到链接关系的目标资源。HCRelationExecutor公开以下函数
doPostForTargetResource: ...
doGetForTargetResource: ...
doPutForTargetResource: ...
doDeleteForTargetResource: ...
这些函数接受一个相当大的参数集。查看API文档获取详细信息。
在发出HTTP请求并返回401(未经授权)时,HCAuthReqdErrorBlk
块(下面描述)会接收到一个HCAuthentication
实例。认证实例包含解析后的WWW-Authenticate
响应头信息(即方案和域)。
可以将HCAuthorization
实例作为参数提供给HAL格式HCRelationExecutor
中的每个doXXXTargetResource: ...
方法。如果提供,将在HTTP请求中包含Authorization
头。授权实例通常封装三个部分:方案、参数和值。存在一个工厂函数,可以简化单个参数/值的HCAuthorization
实例的创建。
doGetForTargetResource:...
的完成/成功块。在这个块中,您将免费获得来自HTTP响应的大部分重要信息,包括
与其它关系执行函数相关的完成块类型提供相同的通用参数。块类型包括:HCPOSTSuccessBlk
、HCPUTSuccessBlk
、HCDELETESuccessBlk
等。
除了支持HATEOAS,PEHateoas-Client在总体上是一层甜美的顶层封装,基于AFNetworking。AFNetworking为其函数提供2种基本的完成块类型,用于成功和失败。而PEHateoas-Client提供完成块用于以下情况:
如你所见,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-type
、location
和last-modified
。payload
条目包含嵌入资源的实际内容。
为了让序列化器能够处理嵌入资源,在构造它时,只需向HCResourceSerializerSupport
的初始化器(HCHalJsonSerializer
从HCResourceSerializerSupport
扩展)的serializersForEmbeddedResources
和actionsForEmbeddedResources
部分提供一个适当的字典。让我们看看一个示例。假设你有以下模型类
@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:@{}];
(每个库都实现为CocoaPod启用的iOS静态库)。