diff --git a/ably-ios/ARTHttpPaginatedResult.h b/ably-ios/ARTHttpPaginatedResult.h index c13a89e32..b24e37791 100644 --- a/ably-ios/ARTHttpPaginatedResult.h +++ b/ably-ios/ARTHttpPaginatedResult.h @@ -10,13 +10,23 @@ #import #import -@interface ARTHttpPaginatedResult : NSObject +@interface ARTHttpPaginatedResult : ARTPaginatedResult -typedef id (^ARTHttpResponseProcessor)(ARTHttpResponse *); +typedef NSArray *(^ARTHttpResponseProcessor)(ARTHttpResponse *); - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithHttp:(ARTHttp *)http current:(id)current contentType:(NSString *)contentType relFirst:(ARTHttpRequest *)relFirst relCurrent:(ARTHttpRequest *)relCurrent relNext:(ARTHttpRequest *)relNext responseProcessor:(ARTHttpResponseProcessor)responseProcessor; -+ (id)makePaginatedRequest:(ARTHttp *)http request:(ARTHttpRequest *)request responseProcessor:(ARTHttpResponseProcessor)responseProcessor cb:(ARTPaginatedResultCb)cb; +- (instancetype)initWithHttp:(ARTHttp *)http + items:(NSArray *)items + contentType:(NSString *)contentType + relFirst:(ARTHttpRequest *)relFirst + relCurrent:(ARTHttpRequest *)relCurrent + relNext:(ARTHttpRequest *)relNext + responseProcessor:(ARTHttpResponseProcessor)responseProcessor; + ++ (id)makePaginatedRequest:(ARTHttp *)http + request:(ARTHttpRequest *)request + responseProcessor:(ARTHttpResponseProcessor)responseProcessor + callback:(ARTPaginatedResultCallback)callback; @end diff --git a/ably-ios/ARTHttpPaginatedResult.m b/ably-ios/ARTHttpPaginatedResult.m index c2a6cca89..3f89f7bd5 100644 --- a/ably-ios/ARTHttpPaginatedResult.m +++ b/ably-ios/ARTHttpPaginatedResult.m @@ -9,82 +9,70 @@ #import "ARTHttpPaginatedResult.h" #import "ARTLog.h" #import "ARTStatus.h" -@interface ARTHttpPaginatedResult () -@property (readonly, strong, nonatomic) ARTHttp *http; -@property (readonly, strong, nonatomic) id currentValue; -@property (readonly, strong, nonatomic) NSString *contentType; -@property (readonly, strong, nonatomic) ARTHttpRequest *relFirst; -@property (readonly, strong, nonatomic) ARTHttpRequest *relCurrent; -@property (readonly, strong, nonatomic) ARTHttpRequest *relNext; -@property (readonly, strong, nonatomic) ARTHttpResponseProcessor responseProcessor; - -@end - -@implementation ARTHttpPaginatedResult +@implementation ARTHttpPaginatedResult { + ARTHttp *_http; + NSString *_contentType; + ARTHttpRequest *_relFirst; + ARTHttpRequest *_relCurrent; + ARTHttpRequest *_relNext; + ARTHttpResponseProcessor _responseProcessor; +} -- (instancetype)initWithHttp:(ARTHttp *)http current:(id)current contentType:(NSString *)contentType relFirst:(ARTHttpRequest *)relFirst relCurrent:(ARTHttpRequest *)relCurrent relNext:(ARTHttpRequest *)relNext responseProcessor:(ARTHttpResponseProcessor)responseProcessor { +- (instancetype)initWithHttp:(ARTHttp *)http items:(NSArray *)items contentType:(NSString *)contentType relFirst:(ARTHttpRequest *)relFirst relCurrent:(ARTHttpRequest *)relCurrent relNext:(ARTHttpRequest *)relNext responseProcessor:(ARTHttpResponseProcessor)responseProcessor { self = [super init]; if (self) { _http = http; - _currentValue = current; - _contentType = contentType; + + _items = [items copy]; + _contentType = [contentType copy]; + + _hasFirst = !!relFirst; _relFirst = relFirst; + + _hasCurrent = !!relCurrent; _relCurrent = relCurrent; + + _hasNext = !!relNext; _relNext = relNext; + _responseProcessor = responseProcessor; } return self; } -- (id)currentItems { - return self.currentValue; -} - -- (BOOL)hasFirst { - return !!self.relFirst; -} - -- (BOOL)hasCurrent { - return !!self.relCurrent; -} - -- (BOOL)hasNext { - return !!self.relNext; -} - -- (void)first:(ARTPaginatedResultCb)cb { - [ARTHttpPaginatedResult makePaginatedRequest:self.http request:self.relFirst responseProcessor:self.responseProcessor cb:cb]; +- (void)first:(ARTPaginatedResultCallback)callback { + [ARTHttpPaginatedResult makePaginatedRequest:_http request:_relFirst responseProcessor:_responseProcessor callback:callback]; } -- (void)current:(ARTPaginatedResultCb)cb { - [ARTHttpPaginatedResult makePaginatedRequest:self.http request:self.relCurrent responseProcessor:self.responseProcessor cb:cb]; +- (void)current:(ARTPaginatedResultCallback)callback { + [ARTHttpPaginatedResult makePaginatedRequest:_http request:_relCurrent responseProcessor:_responseProcessor callback:callback]; } -- (void)next:(ARTPaginatedResultCb)cb { - [ARTHttpPaginatedResult makePaginatedRequest:self.http request:self.relNext responseProcessor:self.responseProcessor cb:cb]; +- (void)next:(ARTPaginatedResultCallback)callback { + [ARTHttpPaginatedResult makePaginatedRequest:_http request:_relNext responseProcessor:_responseProcessor callback:callback]; } -+ (id)makePaginatedRequest:(ARTHttp *)http request:(ARTHttpRequest *)request responseProcessor:(ARTHttpResponseProcessor)responseProcessor cb:(ARTPaginatedResultCb)cb { ++ (id)makePaginatedRequest:(ARTHttp *)http request:(ARTHttpRequest *)request responseProcessor:(ARTHttpResponseProcessor)responseProcessor callback:(ARTPaginatedResultCallback)callback { return [http makeRequest:request cb:^(ARTHttpResponse *response) { if (!response) { ARTErrorInfo * info = [[ARTErrorInfo alloc] init]; [info setCode:40000 message:@"ARTHttpPaginatedResult got no response"]; [ARTStatus state:ARTStatusError info:info]; - cb([ARTStatus state:ARTStatusError info:info], nil); + callback([ARTStatus state:ARTStatusError info:info], nil); return; } if (response.status < 200 || response.status >= 300) { ARTErrorInfo * info = [[ARTErrorInfo alloc] init]; [info setCode:40000 message:[NSString stringWithFormat:@"ARTHttpPaginatedResult response.status invalid: %d", response.status]]; - cb([ARTStatus state:ARTStatusError info:info], nil); + callback([ARTStatus state:ARTStatusError info:info], nil); return; } - id currentValue = responseProcessor(response); + NSArray *items = responseProcessor(response); NSString *contentType = response.contentType; NSDictionary *links = response.links; @@ -93,7 +81,13 @@ - (void)next:(ARTPaginatedResultCb)cb { ARTHttpRequest *currentRelRequest = [request requestWithRelativeUrl:[links objectForKey:@"current"]]; ARTHttpRequest *nextRelRequest = [request requestWithRelativeUrl:[links objectForKey:@"next"]]; - cb(ARTStatusOk, [[ARTHttpPaginatedResult alloc] initWithHttp:http current:currentValue contentType:contentType relFirst:firstRelRequest relCurrent:currentRelRequest relNext:nextRelRequest responseProcessor:responseProcessor]); + ARTPaginatedResult *result = [[ARTHttpPaginatedResult alloc] initWithHttp:http + items:items + contentType:contentType + relFirst:firstRelRequest + relCurrent:currentRelRequest + relNext:nextRelRequest responseProcessor:responseProcessor]; + callback([ARTStatus state:ARTStatusOk], result); }]; } diff --git a/ably-ios/ARTPaginatedResult.h b/ably-ios/ARTPaginatedResult.h index f83f8f0ae..eec388b35 100644 --- a/ably-ios/ARTPaginatedResult.h +++ b/ably-ios/ARTPaginatedResult.h @@ -9,16 +9,26 @@ #import #import -@protocol ARTPaginatedResult +@interface ARTPaginatedResult : NSObject { + @protected + NSArray *_items; + BOOL _hasFirst; + BOOL _hasCurrent; + BOOL _hasNext; +} -- (id)currentItems; -- (BOOL)hasFirst; -- (BOOL)hasCurrent; -- (BOOL)hasNext; +@property (nonatomic, strong, readonly) NSArray *items; -typedef void (^ARTPaginatedResultCb)(ARTStatus *status, id result); -- (void)first:(ARTPaginatedResultCb)cb; -- (void)current:(ARTPaginatedResultCb)cb; -- (void)next:(ARTPaginatedResultCb)cb; +@property (nonatomic, readonly) BOOL hasFirst; +@property (nonatomic, readonly) BOOL hasCurrent; +@property (nonatomic, readonly) BOOL hasNext; + +@property (nonatomic, readonly) BOOL isLast; + +typedef void(^ARTPaginatedResultCallback)(ARTStatus *status, ARTPaginatedResult *result); + +- (void)first:(ARTPaginatedResultCallback)callback; +- (void)current:(ARTPaginatedResultCallback)callback; +- (void)next:(ARTPaginatedResultCallback)callback; @end diff --git a/ably-ios/ARTPaginatedResult.m b/ably-ios/ARTPaginatedResult.m new file mode 100644 index 000000000..ac0d7f0fc --- /dev/null +++ b/ably-ios/ARTPaginatedResult.m @@ -0,0 +1,29 @@ +// +// ARTPaginatedResult.m +// ably +// +// Created by Yavor Georgiev on 10.08.15. +// Copyright (c) 2015 г. Ably. All rights reserved. +// + +#import "ARTPaginatedResult.h" + +@implementation ARTPaginatedResult + +- (void)first:(ARTPaginatedResultCallback)callback { + NSAssert(false, @"-[ARTPaginatedResult first] should always be overriden."); +} + +- (void)current:(ARTPaginatedResultCallback)callback { + NSAssert(false, @"-[ARTPaginatedResult current] should always be overriden."); +} + +- (void)next:(ARTPaginatedResultCallback)callback { + NSAssert(false, @"-[ARTPaginatedResult next] should always be overriden."); +} + +- (BOOL)isLast { + return !self.hasNext; +} + +@end diff --git a/ably-ios/ARTRealtime.h b/ably-ios/ARTRealtime.h index c147aaf17..f25194f36 100644 --- a/ably-ios/ARTRealtime.h +++ b/ably-ios/ARTRealtime.h @@ -13,6 +13,7 @@ #import #import #import +#import #define ART_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) @@ -54,8 +55,8 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeConnectionState) { - (void)publish:(id)payload withName:(NSString *)name cb:(ARTStatusCallback)cb; - (void)publish:(id)payload cb:(ARTStatusCallback)cb; -- (id)history:(ARTPaginatedResultCb)cb; -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; +- (id)history:(ARTPaginatedResultCallback)callback; +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback; @@ -80,10 +81,10 @@ typedef void (^ARTRealtimeChannelStateCb)(ARTRealtimeChannelState, ARTStatus *); @interface ARTPresence : NSObject - (instancetype) initWithChannel:(ARTRealtimeChannel *) channel; -- (id)get:(ARTPaginatedResultCb) cb; -- (id)getWithParams:(NSDictionary *) queryParams cb:(ARTPaginatedResultCb) cb; -- (id)history:(ARTPaginatedResultCb)cb; -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; +- (id)get:(ARTPaginatedResultCallback)callback; +- (id)getWithParams:(NSDictionary *) queryParams cb:(ARTPaginatedResultCallback)callback; +- (id)history:(ARTPaginatedResultCallback)callback; +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback; - (void)enter:(id)data cb:(ARTStatusCallback)cb; - (void)update:(id)data cb:(ARTStatusCallback)cb; @@ -125,8 +126,8 @@ typedef void (^ARTRealtimeChannelPresenceCb)(ARTPresenceMessage *); typedef void (^ARTRealtimePingCb)(ARTStatus *); - (void)ping:(ARTRealtimePingCb) cb; -- (id)stats:(ARTPaginatedResultCb)cb; -- (id)statsWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; + +- (id)stats:(ARTStatsQuery *)query callback:(ARTPaginatedResultCallback)callback; - (ARTRealtimeChannel *)channel:(NSString *)channelName; - (ARTRealtimeChannel *)channel:(NSString *)channelName cipherParams:(ARTCipherParams *)cipherParams; diff --git a/ably-ios/ARTRealtime.m b/ably-ios/ARTRealtime.m index e7b8ed014..a216ec654 100644 --- a/ably-ios/ARTRealtime.m +++ b/ably-ios/ARTRealtime.m @@ -471,17 +471,17 @@ -(void) throwOnDisconnectedOrFailed { [NSException raise:@"realtime cannot perform action in disconnected or failed state" format:@"state: %d", (int)self.realtime.state]; } } -- (id)history:(ARTPaginatedResultCb)cb { +- (id)history:(ARTPaginatedResultCallback)callback { [self throwOnDisconnectedOrFailed]; - return [self.restChannel history:cb]; + return [self.restChannel history:callback]; } -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback { [self throwOnDisconnectedOrFailed]; if([queryParams objectForKey:@"until_attach"] != nil && self.state != ARTRealtimeChannelAttached) { [NSException raise:@"Cannot ask for history with param untilAttach when not attached" format:@""]; } - return [self.restChannel historyWithParams:queryParams cb:cb]; + return [self.restChannel historyWithParams:queryParams cb:callback]; } @@ -903,12 +903,8 @@ - (void)ping:(ARTRealtimePingCb) cb { [self.transport sendPing]; } -- (id)stats:(ARTPaginatedResultCb)cb { - return [self.rest stats:cb]; -} - -- (id)statsWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { - return [self.rest statsWithParams:queryParams cb:cb]; +- (id)stats:(ARTStatsQuery *)query callback:(ARTPaginatedResultCallback)callback { + return [self.rest stats:query callback:callback]; } - (ARTRealtimeChannel *)channel:(NSString *)channelName { @@ -1591,23 +1587,23 @@ -(instancetype) initWithChannel:(ARTRealtimeChannel *) channel { } return self; } --(id) getWithParams:(NSDictionary *) queryParams cb:(ARTPaginatedResultCb) cb { +-(id) getWithParams:(NSDictionary *) queryParams cb:(ARTPaginatedResultCallback)callback { [self.channel throwOnDisconnectedOrFailed]; - return [self.channel.restChannel.presence getWithParams:queryParams cb:cb]; + return [self.channel.restChannel.presence getWithParams:queryParams cb:callback]; } --(id) get:(ARTPaginatedResultCb) cb { +-(id) get:(ARTPaginatedResultCallback)callback { [self.channel throwOnDisconnectedOrFailed]; - return [self.channel.restChannel.presence get:cb]; + return [self.channel.restChannel.presence get:callback]; } -- (id)history:(ARTPaginatedResultCb)cb { +- (id)history:(ARTPaginatedResultCallback)callback { [self.channel throwOnDisconnectedOrFailed]; - return [self.channel.restChannel.presence history:cb]; + return [self.channel.restChannel.presence history:callback]; } -- (id) historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { +- (id) historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback { [self.channel throwOnDisconnectedOrFailed]; - return [self.channel.restChannel.presence historyWithParams:queryParams cb:cb]; + return [self.channel.restChannel.presence historyWithParams:queryParams cb:callback]; } diff --git a/ably-ios/ARTRest.h b/ably-ios/ARTRest.h index 511f89909..06406a066 100644 --- a/ably-ios/ARTRest.h +++ b/ably-ios/ARTRest.h @@ -11,7 +11,7 @@ #import #import #import - +#import @class ARTLog; @class ARTCipherParams; @@ -24,18 +24,18 @@ - (id)publish:(id)payload withName:(NSString *)name cb:(ARTStatusCallback)cb; - (id)publish:(id)payload cb:(ARTStatusCallback)cb; -- (id)history:(ARTPaginatedResultCb)cb; -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; +- (id)history:(ARTPaginatedResultCallback)callback; +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback; @property (readonly, strong, nonatomic) ARTRestPresence *presence; @end @interface ARTRestPresence : NSObject - (instancetype) initWithChannel:(ARTRestChannel *) channel; -- (id)get:(ARTPaginatedResultCb)cb; -- (id)getWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; -- (id)history:(ARTPaginatedResultCb)cb; -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; +- (id)get:(ARTPaginatedResultCallback)callback; +- (id)getWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback; +- (id)history:(ARTPaginatedResultCallback)callback; +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback; @end @interface ARTRest : NSObject @@ -50,8 +50,7 @@ - (id) token:(ARTAuthTokenParams *) keyName tokenCb:(void (^)(ARTStatus * status, ARTTokenDetails *)) cb; - (id)time:(void(^)(ARTStatus * status, NSDate *time))cb; -- (id)stats:(ARTPaginatedResultCb)cb; -- (id)statsWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb; +- (id)stats:(ARTStatsQuery *)query callback:(ARTPaginatedResultCallback)callback; - (id)internetIsUp:(void (^)(bool isUp)) cb; - (ARTRestChannel *)channel:(NSString *)channelName; - (ARTRestChannel *)channel:(NSString *)channelName cipherParams:(ARTCipherParams *)cipherParams; diff --git a/ably-ios/ARTRest.m b/ably-ios/ARTRest.m index 83cfbb6b3..d26290bee 100644 --- a/ably-ios/ARTRest.m +++ b/ably-ios/ARTRest.m @@ -134,11 +134,11 @@ + (instancetype)channelWithRest:(ARTRest *)rest name:(NSString *)name cipherPara } } -- (id)history:(ARTPaginatedResultCb)cb { - return [self historyWithParams:nil cb:cb]; +- (id)history:(ARTPaginatedResultCallback)callback { + return [self historyWithParams:nil cb:callback]; } -- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { +- (id)historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback { [self.rest throwOnHighLimitCheck:queryParams]; return [self.rest withAuthHeaders:^(NSDictionary *authHeaders) { NSString *relUrl = [NSString stringWithFormat:@"%@/messages", self.basePath]; @@ -149,7 +149,7 @@ + (instancetype)channelWithRest:(ARTRest *)rest name:(NSString *)name cipherPara return [messages artMap:^id(ARTMessage *message) { return [message decode:self.payloadEncoder]; }]; - } cb:cb]; + } callback:callback]; }]; } @@ -260,10 +260,6 @@ -(ARTAuth *) auth { } -- (id)stats:(ARTPaginatedResultCb)cb { - return [self statsWithParams:nil cb:cb]; -} - -(void) throwOnHighLimitCheck:(NSDictionary *) params { NSString * limit = [params valueForKey:@"limit"]; if(!limit) { @@ -275,14 +271,22 @@ -(void) throwOnHighLimitCheck:(NSDictionary *) params { } } -- (id)statsWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { - [self throwOnHighLimitCheck:queryParams]; +- (id)stats:(ARTStatsQuery *)query callback:(ARTPaginatedResultCallback)callback { + if (query.limit > 1000) { + [NSException raise:@"AblyException" format:@"Cannot set a query limit over 1000"]; + } + if (query.start && query.end && [query.start compare:query.end] == NSOrderedDescending) { + [NSException raise:@"AblyException" format:@"The query start date cannot be more recent than the query end date"]; + } + return [self withAuthHeaders:^(NSDictionary *authHeaders) { - ARTHttpRequest *req = [[ARTHttpRequest alloc] initWithMethod:@"GET" url:[self resolveUrl:@"/stats" queryParams:queryParams] headers:authHeaders body:nil]; + NSURLComponents *requestUrl = [NSURLComponents componentsWithURL:[self resolveUrl:@"/stats"] resolvingAgainstBaseURL:YES]; + requestUrl.queryItems = [query asQueryItems]; + ARTHttpRequest *req = [[ARTHttpRequest alloc] initWithMethod:@"GET" url:requestUrl.URL headers:authHeaders body:nil]; return [ARTHttpPaginatedResult makePaginatedRequest:self.http request:req responseProcessor:^(ARTHttpResponse *response) { id encoder = [self.encoders objectForKey:response.contentType]; return [encoder decodeStats:response.body]; - } cb:cb]; + } callback:callback]; }]; } @@ -495,11 +499,11 @@ -(instancetype) initWithChannel:(ARTRestChannel *)channel { return self; } -- (id)get:(ARTPaginatedResultCb)cb { - return [self getWithParams:nil cb:cb]; +- (id)get:(ARTPaginatedResultCallback)callback { + return [self getWithParams:nil cb:callback]; } -- (id)getWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { +- (id)getWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback { [self.channel.rest throwOnHighLimitCheck:queryParams]; return [self.channel.rest withAuthHeaders:^(NSDictionary *authHeaders) { NSString *relUrl = [NSString stringWithFormat:@"%@/presence", self.channel.basePath]; @@ -510,15 +514,15 @@ -(instancetype) initWithChannel:(ARTRestChannel *)channel { return [messages artMap:^id(ARTPresenceMessage *pm) { return [pm decode:self.channel.payloadEncoder]; }]; - } cb:cb]; + } callback:callback]; }]; } -- (id)history:(ARTPaginatedResultCb)cb { - return [self historyWithParams:nil cb:cb]; +- (id)history:(ARTPaginatedResultCallback)callback { + return [self historyWithParams:nil cb:callback]; } -- (id) historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCb)cb { +- (id) historyWithParams:(NSDictionary *)queryParams cb:(ARTPaginatedResultCallback)callback { [self.channel.rest throwOnHighLimitCheck:queryParams]; return [self.channel.rest withAuthHeaders:^(NSDictionary *authHeaders) { NSString *relUrl = [NSString stringWithFormat:@"%@/presence/history", self.channel.basePath]; @@ -529,7 +533,7 @@ -(instancetype) initWithChannel:(ARTRestChannel *)channel { return [messages artMap:^id(ARTPresenceMessage *pm) { return [pm decode:self.channel.payloadEncoder]; }]; - } cb:cb]; + } callback:callback]; }]; } diff --git a/ably-ios/ARTStats.h b/ably-ios/ARTStats.h index b8c1a141b..eb2e9b5e9 100644 --- a/ably-ios/ARTStats.h +++ b/ably-ios/ARTStats.h @@ -8,6 +8,33 @@ #import +typedef NS_ENUM(NSUInteger, ARTQueryDirection) { + ARTQueryDirectionForwards, + ARTQueryDirectionBackwards +}; + +typedef NS_ENUM(NSUInteger, ARTStatsUnit) { + ARTStatsUnitMinute, + ARTStatsUnitHour, + ARTStatsUnitDay, + ARTStatsUnitMonth +}; + +@interface ARTStatsQuery : NSObject + +@property (nonatomic, strong) NSDate *start; +@property (nonatomic, strong) NSDate *end; + +@property (nonatomic, assign) uint64_t limit; + +@property (nonatomic, assign) ARTQueryDirection direction; + +@property (nonatomic, assign) ARTStatsUnit unit; + +- (NSArray *)asQueryItems; + +@end + @interface ARTStatsMessageCount : NSObject @property (readonly, assign, nonatomic) double count; diff --git a/ably-ios/ARTStats.m b/ably-ios/ARTStats.m index 083e75100..97143bffb 100644 --- a/ably-ios/ARTStats.m +++ b/ably-ios/ARTStats.m @@ -8,6 +8,61 @@ #import "ARTStats.h" +@implementation ARTStatsQuery + +- (instancetype)init { + if (self = [super init]) { + _limit = 100; + _direction = ARTQueryDirectionBackwards; + _unit = ARTStatsUnitMinute; + } + + return self; +} + +static NSString *statsUnitToString(ARTStatsUnit unit) { + switch (unit) { + case ARTStatsUnitMonth: + return @"month"; + case ARTStatsUnitDay: + return @"day"; + case ARTStatsUnitHour: + return @"hour"; + case ARTStatsUnitMinute: + default: + return @"minute"; + } +} + +static NSString *queryDirectionToString(ARTQueryDirection direction) { + switch (direction) { + case ARTQueryDirectionForwards: + return @"forwards"; + case ARTQueryDirectionBackwards: + default: + return @"backwards"; + } +} + +- (NSArray *)asQueryItems { + NSMutableArray *items = [NSMutableArray array]; + + if (self.start) { + [items addObject:[NSURLQueryItem queryItemWithName:@"start" value:[NSString stringWithFormat:@"%llu", (uint64_t)(self.start.timeIntervalSince1970 * 1000)]]]; + } + if (self.end) { + [items addObject:[NSURLQueryItem queryItemWithName:@"end" value:[NSString stringWithFormat:@"%llu", (uint64_t)(self.end.timeIntervalSince1970 * 1000)]]]; + } + + [items addObject:[NSURLQueryItem queryItemWithName:@"limit" value:[NSString stringWithFormat:@"%llu", self.limit]]]; + [items addObject:[NSURLQueryItem queryItemWithName:@"direction" value:queryDirectionToString(self.direction)]]; + [items addObject:[NSURLQueryItem queryItemWithName:@"unit" value:statsUnitToString(self.unit)]]; + + return [items copy]; +} + +@end + @implementation ARTStatsMessageCount - (instancetype)initWithCount:(double)count data:(double)data { diff --git a/ably-ios/ably.h b/ably-ios/ably.h index 7d07555ba..f1f8e7226 100644 --- a/ably-ios/ably.h +++ b/ably-ios/ably.h @@ -26,4 +26,5 @@ FOUNDATION_EXPORT const unsigned char ablyVersionString[]; #import #import #import -#import \ No newline at end of file +#import +#import \ No newline at end of file diff --git a/ably.xcodeproj/project.pbxproj b/ably.xcodeproj/project.pbxproj index adf7b341e..6cad411c1 100644 --- a/ably.xcodeproj/project.pbxproj +++ b/ably.xcodeproj/project.pbxproj @@ -51,6 +51,9 @@ 1CD8DCA01B1C7315007EAF36 /* ARTDefault.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CD8DC9E1B1C7315007EAF36 /* ARTDefault.m */; }; 1CEFC60E1B0F9EF500D84463 /* ARTTokenDetails+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 1CEFC60D1B0F9EF500D84463 /* ARTTokenDetails+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2EBDBEEBF881A49F795F1D2E /* Pods_ablySpec.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52CD0A2BB89BAA3D8353FBF3 /* Pods_ablySpec.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 850BFB4C1B79323C009D0ADD /* ARTPaginatedResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 850BFB4A1B79323C009D0ADD /* ARTPaginatedResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 850BFB4D1B79323C009D0ADD /* ARTPaginatedResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */; }; + 853ED7C41B7A1A3C006F1C6F /* RestClient.stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853ED7C31B7A1A3C006F1C6F /* RestClient.stats.swift */; }; 856AAC8F1B6E304B00B07119 /* ably.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96BF61311A35B2AB004CF2B3 /* ably.framework */; }; 856AAC971B6E30C800B07119 /* TestUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856AAC961B6E30C800B07119 /* TestUtilities.swift */; }; 856AAC991B6E312F00B07119 /* RestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 856AAC981B6E312F00B07119 /* RestClient.swift */; }; @@ -66,9 +69,8 @@ 961343E91A432E7C006DC822 /* ARTPayload.m in Sources */ = {isa = PBXBuildFile; fileRef = 961343E71A432E7C006DC822 /* ARTPayload.m */; }; 967A43211A39AEAF00E4CE23 /* ARTNSArray+ARTFunctional.h in Headers */ = {isa = PBXBuildFile; fileRef = 967A431F1A39AEAF00E4CE23 /* ARTNSArray+ARTFunctional.h */; }; 967A43221A39AEAF00E4CE23 /* ARTNSArray+ARTFunctional.m in Sources */ = {isa = PBXBuildFile; fileRef = 967A43201A39AEAF00E4CE23 /* ARTNSArray+ARTFunctional.m */; }; - 96A507951A370F860077CDF8 /* ARTStats.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A507931A370F860077CDF8 /* ARTStats.h */; }; + 96A507951A370F860077CDF8 /* ARTStats.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A507931A370F860077CDF8 /* ARTStats.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96A507961A370F860077CDF8 /* ARTStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A507941A370F860077CDF8 /* ARTStats.m */; }; - 96A507991A371E910077CDF8 /* ARTPaginatedResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A507971A371E910077CDF8 /* ARTPaginatedResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96A5079D1A371F800077CDF8 /* ARTHttpPaginatedResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A5079B1A371F800077CDF8 /* ARTHttpPaginatedResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96A5079E1A371F800077CDF8 /* ARTHttpPaginatedResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 96A5079C1A371F800077CDF8 /* ARTHttpPaginatedResult.m */; }; 96A507A11A377AA50077CDF8 /* ARTPresenceMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -190,6 +192,9 @@ 1CD8DC9E1B1C7315007EAF36 /* ARTDefault.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTDefault.m; sourceTree = ""; }; 1CEFC60D1B0F9EF500D84463 /* ARTTokenDetails+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTTokenDetails+Private.h"; sourceTree = ""; }; 52CD0A2BB89BAA3D8353FBF3 /* Pods_ablySpec.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ablySpec.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 850BFB4A1B79323C009D0ADD /* ARTPaginatedResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPaginatedResult.h; sourceTree = ""; }; + 850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTPaginatedResult.m; sourceTree = ""; }; + 853ED7C31B7A1A3C006F1C6F /* RestClient.stats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestClient.stats.swift; sourceTree = ""; }; 856AAC8A1B6E304B00B07119 /* ablySpec.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ablySpec.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 856AAC8E1B6E304B00B07119 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 856AAC951B6E30C800B07119 /* ablySpec-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ablySpec-Bridging-Header.h"; sourceTree = ""; }; @@ -210,7 +215,6 @@ 967A43201A39AEAF00E4CE23 /* ARTNSArray+ARTFunctional.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ARTNSArray+ARTFunctional.m"; sourceTree = ""; }; 96A507931A370F860077CDF8 /* ARTStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTStats.h; sourceTree = ""; }; 96A507941A370F860077CDF8 /* ARTStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTStats.m; sourceTree = ""; }; - 96A507971A371E910077CDF8 /* ARTPaginatedResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPaginatedResult.h; sourceTree = ""; }; 96A5079B1A371F800077CDF8 /* ARTHttpPaginatedResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTHttpPaginatedResult.h; sourceTree = ""; }; 96A5079C1A371F800077CDF8 /* ARTHttpPaginatedResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTHttpPaginatedResult.m; sourceTree = ""; }; 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTPresenceMessage.h; sourceTree = ""; }; @@ -325,6 +329,7 @@ 856AAC961B6E30C800B07119 /* TestUtilities.swift */, 856AAC951B6E30C800B07119 /* ablySpec-Bridging-Header.h */, 856AAC981B6E312F00B07119 /* RestClient.swift */, + 853ED7C31B7A1A3C006F1C6F /* RestClient.stats.swift */, ); path = ablySpec; sourceTree = ""; @@ -371,7 +376,8 @@ 96BF616F1A35FB7C004CF2B3 /* ARTAuth.m */, 96A507931A370F860077CDF8 /* ARTStats.h */, 96A507941A370F860077CDF8 /* ARTStats.m */, - 96A507971A371E910077CDF8 /* ARTPaginatedResult.h */, + 850BFB4A1B79323C009D0ADD /* ARTPaginatedResult.h */, + 850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */, 96A5079B1A371F800077CDF8 /* ARTHttpPaginatedResult.h */, 96A5079C1A371F800077CDF8 /* ARTHttpPaginatedResult.m */, 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */, @@ -483,12 +489,12 @@ 96BF615E1A35C1C8004CF2B3 /* ARTTypes.h in Headers */, 1C1EC3FA1AE26A8B00AAADD7 /* ARTStatus.h in Headers */, 96E408431A38939E00087F77 /* ARTProtocolMessage.h in Headers */, - 96A507991A371E910077CDF8 /* ARTPaginatedResult.h in Headers */, 96BF61581A35B52C004CF2B3 /* ARTHttp.h in Headers */, 96BF61641A35CDE1004CF2B3 /* ARTMessage.h in Headers */, 96BF61701A35FB7C004CF2B3 /* ARTAuth.h in Headers */, 96A507A11A377AA50077CDF8 /* ARTPresenceMessage.h in Headers */, 1C2B0FFD1B136A6D00E3633C /* ARTPresenceMap.h in Headers */, + 850BFB4C1B79323C009D0ADD /* ARTPaginatedResult.h in Headers */, 1CD8DC9F1B1C7315007EAF36 /* ARTDefault.h in Headers */, 96A5079D1A371F800077CDF8 /* ARTHttpPaginatedResult.h in Headers */, 1C8065051AE7C8FA00D49357 /* ARTPayload+Private.h in Headers */, @@ -732,6 +738,7 @@ files = ( 856AAC991B6E312F00B07119 /* RestClient.swift in Sources */, 856AAC971B6E30C800B07119 /* TestUtilities.swift in Sources */, + 853ED7C41B7A1A3C006F1C6F /* RestClient.stats.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -748,6 +755,7 @@ 96BF61591A35B52C004CF2B3 /* ARTHttp.m in Sources */, 1C578E201B3435CA00EF46EC /* ARTFallback.m in Sources */, 96A507B61A37881C0077CDF8 /* ARTNSDate+ARTUtil.m in Sources */, + 850BFB4D1B79323C009D0ADD /* ARTPaginatedResult.m in Sources */, 961343D91A42E0B7006DC822 /* ARTClientOptions.m in Sources */, 96BF615F1A35C1C8004CF2B3 /* ARTTypes.m in Sources */, 96A507AE1A3780F60077CDF8 /* ARTJsonEncoder.m in Sources */, @@ -823,7 +831,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = ablySpec/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = io.ably.ablySpec; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -841,7 +849,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = ablySpec/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = io.ably.ablySpec; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ablySpec/RestClient.stats.swift b/ablySpec/RestClient.stats.swift new file mode 100644 index 000000000..fa359bf51 --- /dev/null +++ b/ablySpec/RestClient.stats.swift @@ -0,0 +1,349 @@ +// +// RestClient.stats.swift +// ably +// +// Created by Yavor Georgiev on 11.08.15. +// Copyright (c) 2015 г. Ably. All rights reserved. +// + +import Nimble +import Quick +import ably +import SwiftyJSON +import Foundation + +private func postTestStats(stats: JSON) -> ARTClientOptions { + let options = AblyTests.setupOptions(AblyTests.jsonRestOptions); + let key = ("\(options.authOptions.keyName):\(options.authOptions.keySecret)" as NSString) + .dataUsingEncoding(NSUTF8StringEncoding)! + .base64EncodedStringWithOptions(NSDataBase64EncodingOptions(0)) + + let request = NSMutableURLRequest(URL: NSURL(string: "https://\(restHost)/stats")!) + request.HTTPMethod = "POST" + request.HTTPBody = stats.rawData() + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("Basic \(key)", forHTTPHeaderField: "Authorization") + + var responseError: NSError? + var httpResponse: NSHTTPURLResponse? + var requestCompleted = false + + NSURLSession.sharedSession() + .dataTaskWithRequest(request) { _, response, error in + responseError = error + httpResponse = response as? NSHTTPURLResponse + requestCompleted = true + }.resume() + + while !requestCompleted { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, CFTimeInterval(0.1), Boolean(0)) + } + + if let error = responseError { + XCTFail(error.localizedDescription) + } else if let response = httpResponse { + if response.statusCode != 201 { + XCTFail("Posting stats fixtures failed") + } + } + + return options +} + +private func queryStats(client: ARTRest, query: ARTStatsQuery) -> ARTPaginatedResult { + var stats: ARTPaginatedResult? + var status: ARTStatus? + client.stats(query, callback: { (statsStatus, result) in + stats = result + status = statsStatus + }) + + while status == nil { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, CFTimeInterval(0.1), Boolean(0)) + } + + if status!.status != .StatusOk { + XCTFail(status!.errorInfo.message) + } + + return stats! +} + +private func getPage(paginator: (ARTPaginatedResultCallback!) -> Void) -> ARTPaginatedResult { + var newResult: ARTPaginatedResult? + var status: ARTStatus? + paginator({ (paginatorStatus, paginatorResult) in + newResult = paginatorResult + status = paginatorStatus + }) + + while status == nil { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, CFTimeInterval(0.1), Boolean(0)) + } + + if status!.status != .StatusOk { + XCTFail(status!.errorInfo.message) + } + + return newResult! +} + +class RestClientStats: QuickSpec { + override func spec() { + describe("RestClient") { + // RSC6 + context("stats") { + // RSC6a + context("result") { + let calendar = NSCalendar(identifier: NSCalendarIdentifierGregorian)! + let dateComponents = NSDateComponents() + dateComponents.year = calendar.component(.CalendarUnitYear, fromDate: NSDate()) - 1 + dateComponents.month = 2 + dateComponents.day = 3 + dateComponents.hour = 16 + dateComponents.minute = 3 + let date = calendar.dateFromComponents(dateComponents)! + let dateFormatter = NSDateFormatter() + dateFormatter.timeZone = NSTimeZone(name: "UTC") + dateFormatter.dateFormat = "YYYY-MM-dd:HH:mm" + + let statsFixtures: JSON = [ + [ + "intervalId": dateFormatter.stringFromDate(date), // 20XX-02-03:16:03 + "inbound": [ "realtime": [ "messages": [ "count": 50, "data": 5000 ] ] ], + "outbound": [ "realtime": [ "messages": [ "count": 20, "data": 2000 ] ] ] + ], + [ + "intervalId": dateFormatter.stringFromDate(date.dateByAddingTimeInterval(60)), // 20XX-02-03:16:04 + "inbound": [ "realtime": [ "messages": [ "count": 60, "data": 6000 ] ] ], + "outbound": [ "realtime": [ "messages": [ "count": 10, "data": 1000 ] ] ] + ], + [ + "intervalId": dateFormatter.stringFromDate(date.dateByAddingTimeInterval(120)), // 20XX-02-03:16:05 + "inbound": [ "realtime": [ "messages": [ "count": 70, "data": 7000 ] ] ], + "outbound": [ "realtime": [ "messages": [ "count": 40, "data": 4000 ] ] ], + "persisted": [ "presence": [ "count": 20, "data": 2000 ] ], + "connections": [ "tls": [ "peak": 20, "opened": 10 ] ], + "channels": [ "peak": 50, "opened": 30 ], + "apiRequests": [ "succeeded": 50, "failed": 10 ], + "tokenRequests": [ "succeeded": 60, "failed": 20 ] + ] + ] + + var statsOptions = ARTClientOptions() + beforeEach { + statsOptions = postTestStats(statsFixtures) + } + + it("should match minute-level inbound and outbound fixture data (forwards)") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.start = date + query.direction = .Forwards + + let result = queryStats(client, query) + expect(result.items.count).to(equal(3)) + + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + expect(totalInbound).to(equal(50 + 60 + 70)) + + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + expect(totalOutbound).to(equal(20 + 10 + 40)) + } + + it("should match hour-level inbound and outbound fixture data (forwards)") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.start = date + query.direction = .Forwards + query.unit = .Hour + + let result = queryStats(client, query) + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + + expect(result.items.count).to(equal(1)) + expect(totalInbound).to(equal(50 + 60 + 70)) + expect(totalOutbound).to(equal(20 + 10 + 40)) + } + + it("should match day-level inbound and outbound fixture data (forwards)") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = calendar.dateByAddingUnit(.CalendarUnitDay, value: 1, toDate: date, options: NSCalendarOptions(0)) + query.direction = .Forwards + query.unit = .Month + + let result = queryStats(client, query) + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + + expect(result.items.count).to(equal(1)) + expect(totalInbound).to(equal(50 + 60 + 70)) + expect(totalOutbound).to(equal(20 + 10 + 40)) + } + + it("should match month-level inbound and outbound fixture data (forwards)") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = calendar.dateByAddingUnit(.CalendarUnitMonth, value: 1, toDate: date, options: NSCalendarOptions(0)) + query.direction = .Forwards + query.unit = .Month + + let result = queryStats(client, query) + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + + expect(result.items.count).to(equal(1)) + expect(totalInbound).to(equal(50 + 60 + 70)) + expect(totalOutbound).to(equal(20 + 10 + 40)) + } + + it("should contain only one item when limit is 1 (backwards") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = date.dateByAddingTimeInterval(60) // 20XX-02-03:16:04 + query.limit = 1 + + let result = queryStats(client, query) + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + + expect(result.items.count).to(equal(1)) + expect(totalInbound).to(equal(60)) + expect(totalOutbound).to(equal(10)) + } + + it("should contain only one item when limit is 1 (forwards") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = date.dateByAddingTimeInterval(60) // 20XX-02-03:16:04 + query.limit = 1 + query.direction = .Forwards + + let result = queryStats(client, query) + let totalInbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.inbound.all.messages.count }) + let totalOutbound = (result.items as! [ARTStats]).reduce(0, combine: { $0 + $1.outbound.all.messages.count }) + + expect(result.items.count).to(equal(1)) + expect(totalInbound).to(equal(50)) + expect(totalOutbound).to(equal(20)) + } + + it("should be paginated according to the limit (backwards") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = date.dateByAddingTimeInterval(120) // 20XX-02-03:16:05 + query.limit = 1 + + + let firstPage = queryStats(client, query) + expect(firstPage.items.count).to(equal(1)) + expect((firstPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(7000)) + expect(firstPage.hasNext).to(beTrue()) + expect(firstPage.isLast).to(beFalse()) + + let secondPage = getPage(firstPage.next) + expect(secondPage.items.count).to(equal(1)) + expect((secondPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(6000)) + expect(secondPage.hasNext).to(beTrue()) + expect(secondPage.isLast).to(beFalse()) + + let thirdPage = getPage(secondPage.next) + expect(thirdPage.items.count).to(equal(1)) + expect((thirdPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(5000)) + expect(thirdPage.hasFirst).to(beTrue()) + expect(thirdPage.isLast).to(beTrue()) + + let firstPageAgain = getPage(thirdPage.first) + expect(firstPageAgain.items.count).to(equal(1)) + expect((firstPageAgain.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(7000)) + } + + it("should be paginated according to the limit (fowards)") { + let client = ARTRest(options: statsOptions) + let query = ARTStatsQuery() + query.end = date.dateByAddingTimeInterval(120) // 20XX-02-03:16:05 + query.limit = 1 + query.direction = .Forwards + + + let firstPage = queryStats(client, query) + expect(firstPage.items.count).to(equal(1)) + expect((firstPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(5000)) + expect(firstPage.hasNext).to(beTrue()) + expect(firstPage.isLast).to(beFalse()) + + let secondPage = getPage(firstPage.next) + expect(secondPage.items.count).to(equal(1)) + expect((secondPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(6000)) + expect(secondPage.hasNext).to(beTrue()) + expect(secondPage.isLast).to(beFalse()) + + let thirdPage = getPage(secondPage.next) + expect(thirdPage.items.count).to(equal(1)) + expect((thirdPage.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(7000)) + expect(thirdPage.hasFirst).to(beTrue()) + expect(thirdPage.isLast).to(beTrue()) + + let firstPageAgain = getPage(thirdPage.first) + expect(firstPageAgain.items.count).to(equal(1)) + expect((firstPageAgain.items as! [ARTStats])[0].inbound.all.messages.data).to(equal(5000)) + } + } + + // RSC6b + context("query") { + // RSC6b1 + context("start") { + it("should throw when later than end") { + let client = ARTRest(key: "fake:key") + let query = ARTStatsQuery() + + query.start = NSDate.distantFuture() as! NSDate + query.end = NSDate.distantPast() as! NSDate + + expect{ client.stats(query, callback: nil) }.to(raiseException()) + } + } + + // RSC6b2 + context("direction") { + it("should be backwards by default") { + let query = ARTStatsQuery() + + expect(query.direction).to(equal(ARTQueryDirection.Backwards)); + } + } + + // RSC6b3 + context("limit") { + it("should have a default value of 100") { + let query = ARTStatsQuery() + + expect(query.limit).to(equal(100)); + } + + it("should throw when greater than 1000") { + let client = ARTRest(key: "fake:key") + let query = ARTStatsQuery() + + query.limit = 1001; + + expect{ client.stats(query, callback: nil) }.to(raiseException()) + } + } + + // RSC6b4 + context("unit") { + it("should default to minute") { + let query = ARTStatsQuery() + + expect(query.unit).to(equal(ARTStatsUnit.Minute)) + } + } + } + } + } + } +} diff --git a/ablySpec/RestClient.swift b/ablySpec/RestClient.swift index f5c1093e9..f15d6fb61 100644 --- a/ablySpec/RestClient.swift +++ b/ablySpec/RestClient.swift @@ -50,10 +50,6 @@ func getTestToken() -> String { class RestClient: QuickSpec { override func spec() { describe("RestClient") { - beforeEach { - ARTClientOptions.getDefaultRestHost("sandbox-rest.ably.io", modify: true) - } - // RSC1 context("initializer") { it("should accept an API key") { diff --git a/ablySpec/TestUtilities.swift b/ablySpec/TestUtilities.swift index 045154653..a8126bc85 100644 --- a/ablySpec/TestUtilities.swift +++ b/ablySpec/TestUtilities.swift @@ -11,6 +11,15 @@ import Foundation import XCTest import SwiftyJSON import ably +import Quick + +class Configuration : QuickConfiguration { + override class func configure(configuration: Quick.Configuration!) { + configuration.beforeEach { + ARTClientOptions.getDefaultRestHost("sandbox-rest.ably.io", modify: true) + } + } +} func pathForTestResource(resourcePath: String) -> String { let testBundle = NSBundle(forClass: AblyTests.self)