#Web Service
Component strikes to provide zero-coding client to remote web services by annotating responsible classes with specific RFAttributes.
It features:
- Basic & digest authentication
- Custom HTTP Headers configuration
- Multipart data
- On-fly serialization and deserialization
- Error handling
- Direct logging to remote server
- Open Data Protocol support
Web Services are represented by RFWebServiceClient
. Instance is created in place or used as a shared service via RFServices.
Snippet below illustrates basic authentication (this example of the code you can see in tests for ROADWebService project):
RFWebServiceClient+DynamicTest.h
#import <ROAD/ROADWebservice.h>
@interface RFWebServiceClient (DynamicTest)
RF_ATTRIBUTE(RFWebServiceCall, serializationDisabled = YES, method = @"GET", relativePath = @"%%0%%")
- (id<RFWebServiceCancellable>)dynamicTestHttpRequestPath:(NSString *)path success:(void(^)(id result))successBlock failure:(void(^)(NSError *error))failureBlock;
@end
Implementation of authentication logic:
#import "RFWebServiceClient+DynamicTest.h"
...
- (void)authentication {
__block BOOL isFinished = NO;
RFWebServiceClient *client = [[RFWebServiceClient alloc] initWithServiceRoot:@"http://httpbin.org/"];
client.authenticationProvider = [[RFBasicAuthenticationProvider alloc] initWithUser:@"user" password:@"passwd"];
[client dynamicTestHttpRequestPath:@"basic-auth/user/passwd" success:^(id result) {
isFinished = YES; /* reveived data ... */
} failure:^(NSError *error) {
isFinished = YES;
}];
while (!isFinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
if ([client.authenticationProvider isSessionOpened]) {
// Session is opened. You can write your code here.
}
}
##Architecture
Core idea of API is to reduce amount of customization code. This is achieved by taking advantage of dynamic method invocation in Objective-C runtime in conjunction with Attribute-Oriented paradigm.
Custom annotated methods are added to subclass or category on RFWebServiceClient
. Template for dynamic methods is as follows:
- (id<RFWebServiceCancellable>)[param1:(NSString *)param1 ... paramN:(NSString*)paramN]
[prepareBlock:(void(^)(NSMutableURLRequest* serviceRequest)requestBlock]
success:(void(^)(id result))successBlock failure:(void(^)(NSError *error))failureBlock;
All specified params are passed to method attributes which support wildcards:
RFWebServiceCall
RFWebServiceHeader
e.g.
RF_ATTRIBUTE(RFWebServiceHeader, headerFields = @{ @"headerKey1" : @"%%0%%@"})
means that first method parameter will used as value for headerKey1
key
dynamicTestHttpRequestPath
from Snippet has no implementation actually. It is defined and annotated in RFWebServiceClient
interface to give client a clue what to do when invoked.
RF_ATTRIBUTE(RFWebServiceCall, serializationDisabled = YES, method = @"GET", relativePath = @"%%0%%")
- (id<RFWebServiceCancellable>)dynamicTestHttpRequestPath:(NSString *)path success:(void(^)(id result))successBlock failure:(void(^)(NSError *error))failureBlock;
RFWebServiceCall
configures that method implementing remote request should return result data as is (no deserialization), and request type is GET
.
##Interface
RFWebServiceClient
interface is tiny and self-documented:
@interface RFWebServiceClient : RFService
@property (copy, nonatomic) NSString *serviceRoot;
@property (strong, atomic) NSMutableDictionary *sharedHeaders;
@property (strong, nonatomic) id<RFSerializationDelegate> serializationDelegate;
@property (strong, nonatomic) id<RFAuthenticating> authenticationProvider;
@property (strong, nonatomic) id<RFWebServiceRequestProcessing> requestProcessor;
- (id)initWithServiceRoot:(NSString *)serviceRoot;
@end
##Defined Attributes
RFWebServiceClientStatusCodes
. Specify valid status for the client. Class-level only.RFWebServiceCall
. Request description.relativePath
- Specify path relative to service root of web service client for current method;successCodes
- List valid status codes of response for annotated method (array ofNSNumber
orNSValue
range values);overrideGlobalSuccessCodes
- Define whether locally defined status codes override global values;method
- Define HTTP method;serializationRoot
- Whether result should be deserialized (Allowed by default);prototypeClass
- Prototype class for deserialization;postParameter
- Specify number of parameter which will be user as HTTP request body. Numeration starts from 0.
RFWebServiceHeader
. Define HTTP headers for request.RFWebServiceLogger
. Define logging for request. Types listed here.RFWebService
. Attribute for web service client class. It allows to specify service root URL.RFWebServiceErrorHandler
. Define class for error handling. This handler class must implement protocolRFWebServiceErrorHandling
RFWebServiceURLBuilder
. Define class for request URL builder.RFWebServiceURLBuilderParameter
. Marks class as a parameter for URL builder. It will not be serialized or altered with standard mechanism.RFMultipartData
. Set boundaries and, in future, other parameter of multipart request.RFWebServiceSerializer
. Specify custom request serializer. Serializer class must conformRFSerializationDelegate
protocol.RFWebServiceCache
. Alter standard behaviour of caching HTTP responses.maxAge
- Cache expiration time for marked web response.cacheDisabled
- Disables caching for marked web request.
##Basic & digest authentication Authentication is implemented one by one of the providers:
RFBasicAuthenticationProvider
;RFDigestAuthenticationProvider
.
by setting it to web client's authenticationProvider
property.
##Custom authentication
The very common case is to implement protocol RFAuthenticating
. The main idea behind it is following:
- You implement custom init method to supply your provider with necessary data.
- Then you need to implement
- (id<RFWebServiceCancellable>)authenticateWithSuccessBlock:(void(^)(id result))successBlock failureBlock:(void(^)(NSError *error))failureBlock;
method to make some steps to authenticate user on web service. In authentication process you can usewebServiceClient
property to reach web service client related to this provider. But we recommend to use another web client class only for authentication purposes in order to separate authentication logic (in majority of cases it will have different url and need different settings, so it makes sense). The method returns an object that implementsRFWebServiceCancellable
protocol and allows you to cancel authentication process. You are responsible for creating the object in your implementation ofRFAuthenticating
and using it properly to cancel the process of authentication. We suggest you to implementRFWebServiceCancellable
protocol in your Authentication Provider and returnself
from this method. You don't have to finish authentication straight away (some authentication have few steps and can not be done immediately), but when you finished you need to notify client via appropriate block: success or failure.- (id<RFWebServiceCancellable>)authenticate;
should perform the same action but without callback blocks. - You need to write some session invalidation logic and put it into
- (id<RFWebServiceCancellable>)invalidate;
method. The method is assumed to be non-atomic, so it also returns an object that implements RFWebServiceCancellable protocol to let you cancel the process. - Optionally you can implement
- (BOOL)isSessionOpened;
to return current state of authentication. - You can check problems with authentication in
- (void)checkResponse:(NSURLResponse *)response forConnection:(NSURLConnection *)connection;
. This method will be performed after every response from web service client, authentication provider works with. - If you authentication provider need to analyze and respond to authentication challenges, you need to implement
- (void)processAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge forConnection:(NSURLConnection *)connection;
. - If you need to modify somehow request data every time you can implement
- (void)addAuthenticationDataToRequest:(NSMutableURLRequest *)request;
.
Or you can implement custom authentication based on one of following classes:
RFAuthenticationProvider
. Has session invalidation in case of authentication challenge in-processAuthenticationChallenge:
, convenient methods to invoke and release callback blocks and so on.RFConcurrentAuthenticationProvider
, based onRFAuthenticationProvider
and implements concurrent invocation of-authenticate
method.- Or even on top of
RFBasicAuthenticationProvider
orRFDigestAuthenticationProvider
.
##Headers customization
Header fields can be customized by addding RFWebServiceHeader
to request definition:
RF_ATTRIBUTE(RFWebServiceHeader, headerFields = @{@"Accept" : @"application/json"})
In case, there's need to have the same headers in every request in one specific web service client. These header fields can be set in sharedHeaders
propery of your web service client instance.
##Multipart data
Request is counted as multipart data request if it has RFFormData
parameters in method declaration.
RFFormData
has set of basic parameters:
- name;
- data;
- fileName (optional);
- contentType (optional). Default value is
application/octet-stream
.
Multipart request builder will use these parameter to create request like this:
@"\r\n--boundary\r\n" // boundary some random boundary or boundary string from attribute RFMultipartData
@"Content-Disposition: form-data; name=\"name\"; filename=\"fileName\"\r\n" // name and fileName from RFFormData
@"Content-Type: contentType\r\n\r\n" // contentType from RFFormData parameter
... data ... // data from data property of RFFormData parameter
@"\r\n--boundary\r\n"
// Next parameter
Requests with multipart data can be marked with RFMultipartData
with customizable boundary
string.
RFFormData instances could be embedded in NSArray
container.
##On-fly serialization and deserialization
If client's property serializationDelegate
is set to id<RFSerializationDelegate>
instance it is asked to serialize objects passed as request parameters.
Deserialization of request result is automatically done by the same serializationDelegate
and passed to callback block.
Default serializer uses RFAttributedCoder
and attempts to performs deserialization from JSON data.
serializationDelegate
can be specified via attributes too. For example, let's change default JSON serialization/deserializaton on XML:
RF_ATTRIBUTE(RFWebServiceSerializer, serializerClass = [RFXMLSerializer class])
##Error handling
Implemented by RFWebServiceErrorHandler
attribute and applied to request. Error handler should conform to RFWebServiceErrorHandling
protocol and implement
+ (id)validateResponse:(NSURLResponse *)response withData:(NSData *)data;
and return NSError
object on error.
Example of custom error handling is OData error handler - RFODataErrorHandler
.
##Web Service Caching
By default web service client will cache data getting into account standard rules of HTTP protocol caching. Web service client react on following headers:
- Pragma
- Cache-Control
- Expires
- ETag
- Last-Modified
This behaviour can be modified via RFWebServiceCache
attribute.
##Open Data Protocol support Web service partially implements Open Data Protocol specification.
Example below initializes request with entityName
and handles result in a callback block.
webClient = [[RFConcreteWebServiceClient alloc] initWithServiceRoot:@"http://fakeurl.com/mashups/mashupengine"];
RFODataFetchRequest *fetchRequest = [[RFODataFetchRequest alloc] initWithEntityName:[RFODataTestEntity entityName]];
[webClient loadDataWithFetchRequest:fetchRequest success:^(id result) {
// Success
} failure:^(NSError *error) {
// Failure
}];
@interface RFConcreteWebServiceClient : RFWebServiceClient
...
RF_ATTRIBUTE(RFWebServiceCall)
RF_ATTRIBUTE(RFWebServiceHeader, headerFields = @{@"Accept": @"application/json"})
RF_ATTRIBUTE(RFWebServiceURLBuilder, builderClass = [RFODataWebServiceURLBuilder class])
- (id<RFWebServiceCancellable>)loadDataWithFetchRequest:(RFODataFetchRequest *)fetchRequest success:(void(^)(id result))successBlock failure:(void(^)(NSError *error))failureBlock;
...
@end
##Request processing Users can modify the NSMutableURLRequest created during the invocation of dynamic methods by supplying an object conforming the RFWebserviceRequestProcessing protocol to the requestProcessor property of the instance. The protocol has only one mandatory method:
- (void)processRequest:(NSMutableURLRequest *)request attributesOnMethod:(NSArray *)attributes;
This method will be invoked with the generated NSMutableURLRequest and with all the attributes attached to the original stub method. Via this optional callback, one can completely customize the generated request.
See the following code from the related test for more info:
In the dynamic method definition stub file:
RF_ATTRIBUTE(RFWebServiceCall, method = @"GET", relativePath = @"/test")
- (id<RFWebServiceCancellable>)processingRequest:(void(^)(id result))successBlock failure:(void(^)(NSError *error))failureBlock;
In the code where the user is invoking the dynamic method:
RFRequestTestProcessor *testRequestProcessor = [[RFRequestTestProcessor alloc] init];
RFWebServiceClient *client = [[RFWebServiceClient alloc] initWithServiceRoot:@"http://httpbin.org/"];
client.requestProcessor = testRequestProcessor;
[client processingRequest:^(id result) {
/* succcess logic */
} failure:^(NSError *error) {
/* failure logic */
}];
##Download Progress Tracking
Users can monitor and, if necessary, display the download process in the UI, using the RFWebServiceClientDownloadProgressBlock
.
To use RFWebServiceClientDownloadProgressBlock
, you need to pass it as one of the parameters. Then set the parameter index of the block in the progressBlockParameter
(RFWebServiceCall
attribute).
@property (assign, nonatomic) int progressBlockParameter;
RF_ATTRIBUTE(RFWebServiceCall, progressBlockParameter = 0)
- (id<RFWebServiceCancellable>)testDownloadingWithProgressBlock:(void(^)(float progress, long long expectedContentLenght))progressBlock
success:(void(^)(NSDictionary *albumsInfo))successBlock
failure:(void(^)(NSError *error))failureBlock;
progress
- the download status with the value from 0.0 to 1.0;expectedContentLenght
- the total size of your data downloads. Return-1
if the size of query results are not known.
RFWebServiceClientDownloadProgressBlock
is executed in the main thread and called at least 2 times:
- Once the request to web service is started (with parameters
(0. -1)
); - Once the request to web service is finished (with parameters
(1, expectedContentLenght)
); - Several times when new portion of data is downloaded.
Note: RFWebServiceClientDownloadProgressBlock
should be placed before preparation, success and failure block, in order to work properly.
RFWebServiceClient *webClient = [[RFWebServiceClient alloc] init];
[webClient downloadingWithProgressBlock:^(float progress, long long expectedContentLenght) {
/* track your progress here */
} success:^(id response) {
/* succcess logic */
} failure:^(NSError *error) {
/* failure logic */
}];