diff --git a/Source/ARTEncoder.h b/Source/ARTEncoder.h index 82c27d939..b0a679535 100644 --- a/Source/ARTEncoder.h +++ b/Source/ARTEncoder.h @@ -64,6 +64,10 @@ NS_ASSUME_NONNULL_BEGIN // DeviceDetails - (nullable NSData *)encodeDeviceDetails:(ARTDeviceDetails *)deviceDetails error:(NSError *_Nullable *_Nullable)error; - (nullable ARTDeviceDetails *)decodeDeviceDetails:(NSData *)data error:(NSError *_Nullable *_Nullable)error; + +// ChannelDetails +- (nullable ARTChannelDetails *)decodeChannelDetails:(NSData *)data error:(NSError *_Nullable *_Nullable)error; + - (nullable NSArray *)decodeDevicesDetails:(NSData *)data error:(NSError * __autoreleasing *)error; - (nullable ARTDeviceIdentityTokenDetails *)decodeDeviceIdentityTokenDetails:(NSData *)data error:(NSError * __autoreleasing *)error; diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index eb4adc1f5..4280d1260 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -136,6 +136,10 @@ - (ARTDeviceDetails *)decodeDeviceDetails:(NSData *)data error:(NSError **)error return [self deviceDetailsFromDictionary:[self decodeDictionary:data error:nil] error:error]; } +- (ARTChannelDetails *)decodeChannelDetails:(NSData *)data error:(NSError **)error { + return [self channelDetailsFromDictionary:[self decodeDictionary:data error:error]]; +} + - (NSArray *)decodeDevicesDetails:(NSData *)data error:(NSError * __autoreleasing *)error { return [self devicesDetailsFromArray:[self decodeArray:data error:nil] error:error]; } @@ -742,15 +746,44 @@ - (ARTConnectionDetails *)connectionDetailsFromDictionary:(NSDictionary *)input if (!input) { return nil; } + return [[ARTConnectionDetails alloc] initWithClientId:[input artString:@"clientId"] + connectionKey:[input artString:@"connectionKey"] + maxMessageSize:[input artInteger:@"maxMessageSize"] + maxFrameSize:[input artInteger:@"maxFrameSize"] + maxInboundRate:[input artInteger:@"maxInboundRate"] + connectionStateTtl:millisecondsToTimeInterval([input artInteger:@"connectionStateTtl"]) + serverId:[input artString:@"serverId"] + maxIdleInterval:millisecondsToTimeInterval([input artInteger:@"maxIdleInterval"])]; +} + +- (ARTChannelMetrics *)channelMetricsFromDictionary:(NSDictionary *)input { + return [[ARTChannelMetrics alloc] initWithConnections:[input artInteger:@"connections"] + publishers:[input artInteger:@"publishers"] + subscribers:[input artInteger:@"subscribers"] + presenceConnections:[input artInteger:@"presenceConnections"] + presenceMembers:[input artInteger:@"presenceMembers"] + presenceSubscribers:[input artInteger:@"presenceSubscribers"]];; +} + +- (ARTChannelOccupancy *)channelOccupancyFromDictionary:(NSDictionary *)input { + NSDictionary *metricsDict = [input valueForKey:@"metrics"]; + ARTChannelMetrics *metrics = [self channelMetricsFromDictionary:metricsDict]; + ARTChannelOccupancy *occupancy = [[ARTChannelOccupancy alloc] initWithMetrics:metrics]; + return occupancy; +} + +- (ARTChannelStatus *)channelStatusFromDictionary:(NSDictionary *)input { + NSDictionary *occupancyDict = [input valueForKey:@"occupancy"]; + ARTChannelOccupancy *occupancy = [self channelOccupancyFromDictionary:occupancyDict]; + ARTChannelStatus *status = [[ARTChannelStatus alloc] initWithOccupancy:occupancy active:[input artBoolean:@"isActive"]]; + return status; +} - return [[ARTConnectionDetails alloc] initWithClientId:[input artString:@"clientId"] - connectionKey:[input artString:@"connectionKey"] - maxMessageSize:[input artInteger:@"maxMessageSize"] - maxFrameSize:[input artInteger:@"maxFrameSize"] - maxInboundRate:[input artInteger:@"maxInboundRate"] - connectionStateTtl:millisecondsToTimeInterval([input artInteger:@"connectionStateTtl"]) - serverId:[input artString:@"serverId"] - maxIdleInterval:millisecondsToTimeInterval([input artInteger:@"maxIdleInterval"])]; +- (ARTChannelDetails *)channelDetailsFromDictionary:(NSDictionary *)input { + NSDictionary *statusDict = [input valueForKey:@"status"]; + ARTChannelStatus *status = [self channelStatusFromDictionary:statusDict]; + ARTChannelDetails *details = [[ARTChannelDetails alloc] initWithChannelId:[input artString:@"channelId"] status:status]; + return details; } - (NSArray *)statsFromArray:(NSArray *)input { diff --git a/Source/ARTRestChannel.h b/Source/ARTRestChannel.h index 0fe0e61e1..b7c73c84b 100644 --- a/Source/ARTRestChannel.h +++ b/Source/ARTRestChannel.h @@ -15,6 +15,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)history:(nullable ARTDataQuery *)query callback:(ARTPaginatedMessagesCallback)callback error:(NSError *_Nullable *_Nullable)errorPtr; +- (void)status:(ARTChannelDetailsCallback)callback; + - (void)setOptions:(ARTChannelOptions *_Nullable)options; @end diff --git a/Source/ARTRestChannel.m b/Source/ARTRestChannel.m index 6d07a03e9..35d613467 100644 --- a/Source/ARTRestChannel.m +++ b/Source/ARTRestChannel.m @@ -46,6 +46,10 @@ - (BOOL)history:(nullable ARTDataQuery *)query callback:(ARTPaginatedMessagesCal return [_internal history:query callback:callback error:errorPtr]; } +- (void)status:(ARTChannelDetailsCallback)callback { + [_internal status:callback]; +} + - (void)publish:(nullable NSString *)name data:(nullable id)data { [_internal publish:name data:data]; } @@ -214,6 +218,67 @@ - (BOOL)history:(ARTDataQuery *)query callback:(ARTPaginatedMessagesCallback)cal return ret; } +- (void)status:(ARTChannelDetailsCallback)callback { + if (callback) { + ARTChannelDetailsCallback userCallback = callback; + callback = ^(ARTChannelDetails *details, ARTErrorInfo *_Nullable error) { + dispatch_async(self->_userQueue, ^{ + userCallback(details, error); + }); + }; + } + dispatch_async(_queue, ^{ + NSURL *url = [NSURL URLWithString:self->_basePath]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p C:%p (%@) channel details request %@", self->_rest, self, self.name, request]; + + [self->_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable error) { + + if (response.statusCode == 200 /*OK*/) { + NSError *decodeError = nil; + id decoder = self->_rest.encoders[response.MIMEType]; + if (decoder == nil) { + NSString* errorMessage = [NSString stringWithFormat:@"Decoder for MIMEType '%@' wasn't found.", response.MIMEType]; + [self.logger debug:__FILE__ line:__LINE__ message:@"%@: %@", NSStringFromClass(self.class), errorMessage]; + if (callback) { + callback(nil, [ARTErrorInfo createWithCode:ARTErrorUnableToDecodeMessage message:errorMessage]); + } + } + else { + ARTChannelDetails *channelDetails = [decoder decodeChannelDetails:data error:&decodeError]; + if (decodeError) { + [self.logger debug:__FILE__ line:__LINE__ message:@"%@: decode channel details failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + if (callback) { + callback(nil, [ARTErrorInfo createFromNSError:decodeError]); + } + } + else { + [self.logger debug:__FILE__ line:__LINE__ message:@"%@: successfully got channel details %@", NSStringFromClass(self.class), channelDetails.channelId]; + if (callback) { + callback(channelDetails, nil); + } + } + } + } + else { + [self.logger debug:__FILE__ line:__LINE__ message:@"%@: get channel details failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + ARTErrorInfo *errorInfo = nil; + if (error) { + if (self->_rest.options.addRequestIds) { + errorInfo = [ARTErrorInfo wrap:[ARTErrorInfo createFromNSError:error] prepend:[NSString stringWithFormat:@"Request '%@' failed with ", request.URL]]; + } else { + errorInfo = [ARTErrorInfo createFromNSError:error]; + } + } + if (callback) { + callback(nil, errorInfo); + } + } + }]; + }); +} + - (void)internalPostMessages:(id)data callback:(ARTCallback)callback { if (callback) { ARTCallback userCallback = callback; diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 4166b25c1..8c10d7bdc 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -201,6 +201,60 @@ NSString *generateNonce(void); @end +#pragma mark - ARTChannelMetrics + +@interface ARTChannelMetrics : NSObject + +@property (nonatomic, readonly) NSInteger connections; +@property (nonatomic, readonly) NSInteger publishers; +@property (nonatomic, readonly) NSInteger subscribers; +@property (nonatomic, readonly) NSInteger presenceConnections; +@property (nonatomic, readonly) NSInteger presenceMembers; +@property (nonatomic, readonly) NSInteger presenceSubscribers; + +- (instancetype)initWithConnections:(NSInteger)connections + publishers:(NSInteger)publishers + subscribers:(NSInteger)subscribers + presenceConnections:(NSInteger)presenceConnections + presenceMembers:(NSInteger)presenceMembers + presenceSubscribers:(NSInteger)presenceSubscribers; + +@end + +#pragma mark - ARTChannelOccupancy + +@interface ARTChannelOccupancy : NSObject + +@property (nonatomic, strong, readonly) ARTChannelMetrics *metrics; + +- (instancetype)initWithMetrics:(ARTChannelMetrics *)metrics; + +@end + +#pragma mark - ARTChannelStatus + +@interface ARTChannelStatus : NSObject + +@property (nonatomic, readonly) BOOL active; + +@property (nonatomic, strong, readonly) ARTChannelOccupancy *occupancy; + +- (instancetype)initWithOccupancy:(ARTChannelOccupancy *)occupancy active:(BOOL)active; + +@end + +#pragma mark - ARTChannelDetails + +@interface ARTChannelDetails : NSObject + +@property (nonatomic, strong, readonly) NSString *channelId; + +@property (nonatomic, strong, readonly) ARTChannelStatus *status; + +- (instancetype)initWithChannelId:(NSString *)channelId status:(ARTChannelStatus *)status; + +@end + #pragma mark - ARTJsonCompatible @protocol ARTJsonCompatible @@ -264,6 +318,7 @@ typedef void (^ARTChannelStateCallback)(ARTChannelStateChange *stateChange); typedef void (^ARTConnectionStateCallback)(ARTConnectionStateChange *stateChange); typedef void (^ARTPresenceMessageCallback)(ARTPresenceMessage *message); typedef void (^ARTPresenceMessagesCallback)(NSArray *_Nullable result, ARTErrorInfo *_Nullable error); +typedef void (^ARTChannelDetailsCallback)(ARTChannelDetails *_Nullable details, ARTErrorInfo *_Nullable error); typedef void (^ARTStatusCallback)(ARTStatus *status); typedef void (^ARTURLRequestCallback)(NSHTTPURLResponse *_Nullable result, NSData *_Nullable data, NSError *_Nullable error); diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index d45fc0acb..f723ef6cd 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -134,6 +134,71 @@ - (NSString *)description { @end +#pragma mark - ARTChannelMetrics + +@implementation ARTChannelMetrics + +- (instancetype)initWithConnections:(NSInteger)connections + publishers:(NSInteger)publishers + subscribers:(NSInteger)subscribers + presenceConnections:(NSInteger)presenceConnections + presenceMembers:(NSInteger)presenceMembers + presenceSubscribers:(NSInteger)presenceSubscribers { + + if (self = [super init]) { + _connections = connections; + _publishers = publishers; + _subscribers = subscribers; + _presenceConnections = presenceConnections; + _presenceMembers = presenceMembers; + _presenceSubscribers = presenceSubscribers; + } + return self; +} + +@end + +#pragma mark - ARTChannelOccupancy + +@implementation ARTChannelOccupancy + +- (instancetype)initWithMetrics:(ARTChannelMetrics *)metrics { + if (self = [super init]) { + _metrics = metrics; + } + return self; +} + +@end + +#pragma mark - ARTChannelStatus + +@implementation ARTChannelStatus + +- (instancetype)initWithOccupancy:(ARTChannelOccupancy *)occupancy active:(BOOL)active { + if (self = [super init]) { + _occupancy = occupancy; + _active = active; + } + return self; +} + +@end + +#pragma mark - ARTChannelDetails + +@implementation ARTChannelDetails + +- (instancetype)initWithChannelId:(NSString *)channelId status:(ARTChannelStatus *)status { + if (self = [super init]) { + _channelId = channelId; + _status = status; + } + return self; +} + +@end + #pragma mark - ARTEventIdentification @implementation NSString (ARTEventIdentification) diff --git a/Source/Private/ARTNSDictionary+ARTDictionaryUtil.h b/Source/Private/ARTNSDictionary+ARTDictionaryUtil.h index 44379e1bc..078353c1d 100644 --- a/Source/Private/ARTNSDictionary+ARTDictionaryUtil.h +++ b/Source/Private/ARTNSDictionary+ARTDictionaryUtil.h @@ -8,6 +8,7 @@ - (NSArray *)artArray:(id)key; - (NSDictionary *)artDictionary:(id)key; - (NSInteger)artInteger:(id)key; +- (BOOL)artBoolean:(id)key; - (id)artTyped:(Class)cls key:(id)key; diff --git a/Source/Private/ARTNSDictionary+ARTDictionaryUtil.m b/Source/Private/ARTNSDictionary+ARTDictionaryUtil.m index 1893e2534..188f8c966 100644 --- a/Source/Private/ARTNSDictionary+ARTDictionaryUtil.m +++ b/Source/Private/ARTNSDictionary+ARTDictionaryUtil.m @@ -51,4 +51,8 @@ - (NSInteger)artInteger:(id)key { return 0; } +- (BOOL)artBoolean:(id)key { + return [self artInteger:key] != 0; +} + @end diff --git a/Spec/Tests/RestClientChannelTests.swift b/Spec/Tests/RestClientChannelTests.swift index 9caee7d4d..c8122d77a 100644 --- a/Spec/Tests/RestClientChannelTests.swift +++ b/Spec/Tests/RestClientChannelTests.swift @@ -1488,4 +1488,38 @@ class RestClientChannelTests: XCTestCase { } } } + + // RSL8a, CHD2b, CHS2b, CHO2a + func test__047__status__with_subscribers__returns_a_channel_details_object_populated_with_channel_metrics() { + let options = AblyTests.commonAppSetup() + options.clientId = "Client 1" + let rest = ARTRest(options: options) + let realtime = ARTRealtime(options: options) + let channelName = uniqueChannelName() + let realtimeChannel = realtime.channels.get(channelName) + let restChannel = rest.channels.get(channelName) + + waitUntil(timeout: testTimeout) { done in + realtimeChannel.presence.enter(nil) { error in + expect(error).to(beNil()) + done() + } + } + waitUntil(timeout: testTimeout) { done in + restChannel.status { details, error in + expect(error).to(beNil()) + guard let details = details else { + fail("Channel details are empty"); done() + return + } + expect(details.status.occupancy.metrics.connections) == 1 // CHM2a + expect(details.status.occupancy.metrics.publishers) == 1 // CHM2e + expect(details.status.occupancy.metrics.subscribers) == 1 // CHM2f + expect(details.status.occupancy.metrics.presenceMembers) == 1 // CHM2c + expect(details.status.occupancy.metrics.presenceConnections) == 1 // CHM2b + expect(details.status.occupancy.metrics.presenceSubscribers) == 1 // CHM2d + done() + } + } + } }