Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Paginated Response #783

Merged
merged 10 commits into from
Sep 21, 2018
40 changes: 40 additions & 0 deletions Ably.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Source/ARTConstants.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// ARTConstants.h
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import <Foundation/Foundation.h>

extern NSString *const ARTHttpHeaderFieldErrorCodeKey;
extern NSString *const ARTHttpHeaderFieldErrorMessageKey;
13 changes: 13 additions & 0 deletions Source/ARTConstants.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// ARTConstants.m
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import "ARTConstants.h"

NSString *const ARTHttpHeaderFieldErrorCodeKey = @"X-Ably-Errorcode";
NSString *const ARTHttpHeaderFieldErrorMessageKey = @"X-Ably-Errormessage";
funkyboy marked this conversation as resolved.
Show resolved Hide resolved

23 changes: 23 additions & 0 deletions Source/ARTHTTPPaginatedResponse+Private.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// ARTHTTPPaginatedResponse+Private.h
// Ably
//
// Created by Yavor Georgiev on 28.08.15.
// Copyright (c) 2015 г. Ably. All rights reserved.
//

#import <Ably/ARTHTTPPaginatedResponse.h>

@class ARTRest;

NS_ASSUME_NONNULL_BEGIN

@interface ARTHTTPPaginatedResponse ()

+ (void)executePaginated:(ARTRest *)rest withRequest:(NSMutableURLRequest *)request andResponseProcessor:(ARTPaginatedResultResponseProcessor)responseProcessor callback:(void (^)(ARTPaginatedResult * _Nullable, ARTErrorInfo * _Nullable))callback UNAVAILABLE_ATTRIBUTE;

+ (void)executePaginated:(ARTRest *)rest withRequest:(NSMutableURLRequest *)request callback:(void (^)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error))callback;

@end

NS_ASSUME_NONNULL_END
37 changes: 37 additions & 0 deletions Source/ARTHTTPPaginatedResponse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// ARTHTTPPaginatedResponse.h
// Ably
//
// Created by Ricardo Pereira on 17/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import <Foundation/Foundation.h>

#import <Ably/ARTPaginatedResult.h>

NS_ASSUME_NONNULL_BEGIN

@interface ARTHTTPPaginatedResponse : ARTPaginatedResult<NSDictionary *>

/// Return the HTTP status code of the response
@property (nonatomic, readonly) NSInteger statusCode;

/// Returns true when the HTTP status code indicates success i.e. 200 <= statusCode < 300
@property (nonatomic, readonly) BOOL success;

/// Returns the error code if the X-Ably-Errorcode HTTP header is sent in the response
@property (nonatomic, readonly) NSInteger errorCode;

/// Returns error message if the X-Ably-Errormessage HTTP header is sent in the response
@property (nullable, nonatomic, readonly) NSString *errorMessage;

/// Returns a dictionary containing all the HTTP header fields of the response header.
@property (nonatomic, readonly) NSDictionary<NSString *, NSString *> *headers;

- (void)first:(void (^)(ARTHTTPPaginatedResponse * _Nullable, ARTErrorInfo * _Nullable))callback;
- (void)next:(void (^)(ARTHTTPPaginatedResponse * _Nullable, ARTErrorInfo * _Nullable))callback;

@end

NS_ASSUME_NONNULL_END
139 changes: 139 additions & 0 deletions Source/ARTHTTPPaginatedResponse.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// ARTHTTPPaginatedResponse.m
// Ably
//
// Created by Ricardo Pereira on 17/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import "ARTHTTPPaginatedResponse.h"

#import "ARTHttp.h"
#import "ARTAuth.h"
#import "ARTRest+Private.h"
#import "ARTPaginatedResult+Private.h"
#import "ARTNSMutableURLRequest+ARTPaginated.h"
#import "ARTNSHTTPURLResponse+ARTPaginated.h"
#import "ARTEncoder.h"
#import "ARTConstants.h"

@implementation ARTHTTPPaginatedResponse {
NSHTTPURLResponse *_response;
}

- (instancetype)initWithResponse:(NSHTTPURLResponse *)response
items:(NSArray *)items
rest:(ARTRest *)rest
relFirst:(NSMutableURLRequest *)relFirst
relCurrent:(NSMutableURLRequest *)relCurrent
relNext:(NSMutableURLRequest *)relNext
responseProcessor:(ARTPaginatedResultResponseProcessor)responseProcessor {
self = [super initWithItems:items rest:rest relFirst:relFirst relCurrent:relCurrent relNext:relNext responseProcessor:responseProcessor];
if (self) {
_response = response;
}
return self;
}

- (NSInteger)statusCode {
return _response.statusCode;
}

- (BOOL)success {
return _response.statusCode >= 200 && _response.statusCode < 300;
}

- (NSInteger)errorCode {
NSString *code = [_response.allHeaderFields valueForKey:ARTHttpHeaderFieldErrorCodeKey];
return [code integerValue];
}

- (NSString *)errorMessage {
NSString *message = [_response.allHeaderFields valueForKey:ARTHttpHeaderFieldErrorMessageKey];
return message;
}

- (NSDictionary<NSString *,NSString *> *)headers {
return _response.allHeaderFields;
}

- (void)first:(void (^)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error))callback {
ART_TRY_OR_REPORT_CRASH_START(self.rest) {
if (callback) {
void (^userCallback)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error) = callback;
callback = ^(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error) {
ART_EXITING_ABLY_CODE(self.rest);
dispatch_async(self.userQueue, ^{
userCallback(result, error);
});
};
}

[self.class executePaginated:self.rest withRequest:self.relFirst callback:callback];
} ART_TRY_OR_REPORT_CRASH_END
}

- (void)next:(void (^)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error))callback {
ART_TRY_OR_REPORT_CRASH_START(self.rest) {
if (callback) {
void (^userCallback)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error) = callback;
callback = ^(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error) {
ART_EXITING_ABLY_CODE(self.rest);
dispatch_async(self.userQueue, ^{
userCallback(result, error);
});
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if looks the same as implemented in first:. Worth extracting it in separate method?


if (!self.relNext) {
// If there is no next page, we can't make a request, so we answer the callback
// with a nil PaginatedResult. That's why the callback has the result as nullable
// anyway. (That, and that it can fail.)
callback(nil, nil);
return;
}

[self.class executePaginated:self.rest withRequest:self.relNext callback:callback];
} ART_TRY_OR_REPORT_CRASH_END
}

+ (void)executePaginated:(ARTRest *)rest withRequest:(NSMutableURLRequest *)request callback:(void (^)(ARTHTTPPaginatedResponse *_Nullable result, ARTErrorInfo *_Nullable error))callback {
ART_TRY_OR_REPORT_CRASH_START(rest) {
[rest.logger debug:__FILE__ line:__LINE__ message:@"HTTP Paginated request: %@", request];

[rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) {
if (error && ![error.domain isEqualToString:ARTAblyErrorDomain]) {
callback(nil, [ARTErrorInfo createFromNSError:error]);
return;
}

[[rest logger] debug:__FILE__ line:__LINE__ message:@"HTTP Paginated response: %@", response];
[[rest logger] debug:__FILE__ line:__LINE__ message:@"HTTP Paginated response data: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];

NSError *decodeError = nil;

ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **errorPtr) {
id<ARTEncoder> encoder = [rest.encoders objectForKey:response.MIMEType];
return [encoder decodeToArray:data error:errorPtr];
};
NSArray *items = error ? nil : responseProcessor(response, data, &decodeError);

if (decodeError) {
callback(nil, [ARTErrorInfo createFromNSError:decodeError]);
return;
}

NSDictionary *links = [response extractLinks];

NSMutableURLRequest *firstRel = [NSMutableURLRequest requestWithPath:links[@"first"] relativeTo:request];
NSMutableURLRequest *currentRel = [NSMutableURLRequest requestWithPath:links[@"current"] relativeTo:request];
NSMutableURLRequest *nextRel = [NSMutableURLRequest requestWithPath:links[@"next"] relativeTo:request];

ARTHTTPPaginatedResponse *result = [[ARTHTTPPaginatedResponse alloc] initWithResponse:response items:items rest:rest relFirst:firstRel relCurrent:currentRel relNext:nextRel responseProcessor:responseProcessor];

callback(result, nil);
}];
} ART_TRY_OR_REPORT_CRASH_END
}

@end
3 changes: 2 additions & 1 deletion Source/ARTHttp.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import "ARTHttp.h"
#import "ARTURLSessionServerTrust.h"
#import "ARTConstants.h"

@interface ARTHttp ()

Expand Down Expand Up @@ -51,7 +52,7 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT
} else {
[self.logger debug:@"%@ %@: statusCode %ld", request.HTTPMethod, request.URL.absoluteString, (long)httpResponse.statusCode];
[self.logger verbose:@"Headers %@", httpResponse.allHeaderFields];
NSString *headerErrorMessage = httpResponse.allHeaderFields[@"X-Ably-ErrorMessage"];
NSString *headerErrorMessage = httpResponse.allHeaderFields[ARTHttpHeaderFieldErrorMessageKey];
funkyboy marked this conversation as resolved.
Show resolved Hide resolved
if (headerErrorMessage && ![headerErrorMessage isEqualToString:@""]) {
[self.logger warn:@"%@", headerErrorMessage];
}
Expand Down
19 changes: 19 additions & 0 deletions Source/ARTNSHTTPURLResponse+ARTPaginated.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ARTNSHTTPURLResponse+ARTPaginated.h
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSHTTPURLResponse (ARTPaginated)

- (nullable NSDictionary *)extractLinks;

@end

NS_ASSUME_NONNULL_END
44 changes: 44 additions & 0 deletions Source/ARTNSHTTPURLResponse+ARTPaginated.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// ARTNSHTTPURLResponse+ARTPaginated.m
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import "ARTNSHTTPURLResponse+ARTPaginated.h"

@implementation NSHTTPURLResponse (ARTPaginated)

- (NSDictionary *)extractLinks {
NSString *linkHeader = self.allHeaderFields[@"Link"];
if (!linkHeader) {
return nil;
}

static NSRegularExpression *linkRegex;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
linkRegex = [NSRegularExpression regularExpressionWithPattern:@"\\s*<([^>]*)>;\\s*rel=\"([^\"]*)\"" options:0 error:nil];
});

NSMutableDictionary *links = [NSMutableDictionary dictionary];

NSArray *matches = [linkRegex matchesInString:linkHeader options:0 range:NSMakeRange(0, linkHeader.length)];
for (NSTextCheckingResult *match in matches) {
NSRange linkUrlRange = [match rangeAtIndex:1];
NSRange linkRelRange = [match rangeAtIndex:2];

NSString *linkUrl = [linkHeader substringWithRange:linkUrlRange];
NSString *linkRels = [linkHeader substringWithRange:linkRelRange];

for (NSString *linkRel in [linkRels componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]) {
[links setObject:linkUrl forKey:linkRel];
}
}

return links;
}

@end
19 changes: 19 additions & 0 deletions Source/ARTNSMutableURLRequest+ARTPaginated.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// ARTNSMutableURLRequest+ARTPaginated.h
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSMutableURLRequest (ARTPaginated)

+ (nullable NSMutableURLRequest *)requestWithPath:(NSString *)path relativeTo:(NSURLRequest *)request;

@end

NS_ASSUME_NONNULL_END
21 changes: 21 additions & 0 deletions Source/ARTNSMutableURLRequest+ARTPaginated.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// ARTNSMutableURLRequest+ARTPaginated.m
// Ably
//
// Created by Ricardo Pereira on 23/08/2018.
// Copyright © 2018 Ably. All rights reserved.
//

#import "ARTNSMutableURLRequest+ARTPaginated.h"

@implementation NSMutableURLRequest (ARTPaginated)

+ (NSMutableURLRequest *)requestWithPath:(NSString *)path relativeTo:(NSURLRequest *)request {
if (!path) {
return nil;
}
NSURL *url = [NSURL URLWithString:path relativeToURL:request.URL];
return [NSMutableURLRequest requestWithURL:url];
}

@end
21 changes: 18 additions & 3 deletions Source/ARTPaginatedResult+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,26 @@ NS_ASSUME_NONNULL_BEGIN

@interface ARTPaginatedResult<ItemType> ()

@property (nonatomic, readonly) ARTRest *rest;
@property (nonatomic, readonly) dispatch_queue_t userQueue;
@property (nonatomic, readonly) dispatch_queue_t queue;
@property (nonatomic, readonly) NSMutableURLRequest *relFirst;
@property (nonatomic, readonly) NSMutableURLRequest *relCurrent;
@property (nonatomic, readonly) NSMutableURLRequest *relNext;

typedef NSArray<ItemType> *_Nullable(^ARTPaginatedResultResponseProcessor)(NSHTTPURLResponse *_Nullable, NSData *_Nullable, NSError *_Nullable *_Nullable);

+ (void)executePaginated:(ARTRest *)rest withRequest:(NSMutableURLRequest *)request
andResponseProcessor:(ARTPaginatedResultResponseProcessor)responseProcessor
callback:(void (^)(ARTPaginatedResult<ItemType> *_Nullable result, ARTErrorInfo *_Nullable error))callback;
- (instancetype)initWithItems:(NSArray *)items
rest:(ARTRest *)rest
relFirst:(NSMutableURLRequest *)relFirst
relCurrent:(NSMutableURLRequest *)relCurrent
relNext:(NSMutableURLRequest *)relNext
responseProcessor:(ARTPaginatedResultResponseProcessor)responseProcessor;

+ (void)executePaginated:(ARTRest *)rest
withRequest:(NSMutableURLRequest *)request
andResponseProcessor:(ARTPaginatedResultResponseProcessor)responseProcessor
callback:(void (^)(ARTPaginatedResult<ItemType> *_Nullable result, ARTErrorInfo *_Nullable error))callback;

@end

Expand Down
Loading