diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java index 2e51aac19..2b0adf993 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMessageCodec.java @@ -1,5 +1,6 @@ package io.ably.flutter.plugin; +import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.function.Consumer; @@ -7,11 +8,13 @@ import io.ably.lib.rest.Auth.TokenDetails; import io.ably.lib.types.ClientOptions; import io.ably.lib.types.Param; +import io.ably.lib.types.ErrorInfo; import io.flutter.plugin.common.StandardMessageCodec; public class AblyMessageCodec extends StandardMessageCodec { private static final byte _valueClientOptions = (byte)128; private static final byte _valueTokenDetails = (byte)129; + private static final byte _errorInfo = (byte)144; private static final byte _valueAblyMessage = (byte)255; @Override @@ -110,4 +113,26 @@ private Object readTokenDetails(final ByteBuffer buffer) { return o; } + + + //HANDLING WRITE + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if(value instanceof ErrorInfo){ + stream.write(_errorInfo); + writeErrorInfo(stream, (ErrorInfo) value); + return; + } + super.writeValue(stream, value); + } + + private void writeErrorInfo(ByteArrayOutputStream stream, ErrorInfo e){ + writeValue(stream, e.code); + writeValue(stream, e.message); + writeValue(stream, e.statusCode); + writeValue(stream, e.href); + writeValue(stream, null); //requestId - not available in ably-java + writeValue(stream, null); //cause - not available in ably-java + } } diff --git a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java index 906594965..ada05a91c 100644 --- a/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java +++ b/android/src/main/java/io/ably/flutter/plugin/AblyMethodCallHandler.java @@ -165,7 +165,7 @@ public void onSuccess() { @Override public void onError(ErrorInfo reason) { - result.error(Integer.toString(reason.code), "Unable to publish message to Ably server; err = " + reason.message, null); + result.error(Integer.toString(reason.code), "Unable to publish message to Ably server; err = " + reason.message, reason); } }); }); diff --git a/example/lib/main.dart b/example/lib/main.dart index dbc321513..fc584fb0d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -116,9 +116,13 @@ class _MyAppState extends State { String name = "Hello"; dynamic data = "Flutter"; print('publishing messages... name "$name", message "$data"'); - await rest.channels.get('test').publish(name, data); - await rest.channels.get('test').publish(name); - await rest.channels.get('test').publish(); + try { + await rest.channels.get('test').publish(name, data); + await rest.channels.get('test').publish(name); + await rest.channels.get('test').publish(); + } on ably.AblyException catch(e) { + print(e.errorInfo); + } print('Message published'); } diff --git a/ios/Classes/AblyFlutterPlugin.m b/ios/Classes/AblyFlutterPlugin.m index 2a91edfc3..623469364 100644 --- a/ios/Classes/AblyFlutterPlugin.m +++ b/ios/Classes/AblyFlutterPlugin.m @@ -4,7 +4,7 @@ // @import Ably; #import "Ably.h" -#import "AblyFlutterReaderWriter.h" +#import "codec/AblyFlutterReaderWriter.h" #import "AblyFlutterMessage.h" #import "AblyFlutter.h" #import "AblyFlutterSurfaceRealtime.h" @@ -65,7 +65,7 @@ -(nullable AblyFlutter *)ablyWithHandle:(NSNumber *)handle; FlutterError errorWithCode:[NSString stringWithFormat: @"%ld", (long)error.code] message:[NSString stringWithFormat:@"Unable to publish message to Ably server; err = %@", [error message]] - details:nil + details:error ]); }else{ result(nil); diff --git a/ios/Classes/codec/AblyFlutterReader.h b/ios/Classes/codec/AblyFlutterReader.h new file mode 100644 index 000000000..45cff704b --- /dev/null +++ b/ios/Classes/codec/AblyFlutterReader.h @@ -0,0 +1,17 @@ +@import Foundation; +@import Flutter; + +NS_ASSUME_NONNULL_BEGIN + +@interface AblyFlutterReader : FlutterStandardReader +@end + +NS_ASSUME_NONNULL_END + + +typedef NS_ENUM(UInt8, _Value) { + _valueClientOptions = 128, + _valueTokenDetails = 129, + _ValueErrorInfo = 144, + _valueAblyMessage = 255, +}; diff --git a/ios/Classes/AblyFlutterReader.m b/ios/Classes/codec/AblyFlutterReader.m similarity index 97% rename from ios/Classes/AblyFlutterReader.m rename to ios/Classes/codec/AblyFlutterReader.m index 165bf91e5..fe387b6ec 100644 --- a/ios/Classes/AblyFlutterReader.m +++ b/ios/Classes/codec/AblyFlutterReader.m @@ -16,12 +16,6 @@ static ARTLogLevel _logLevel(NSNumber *const number) { @implementation AblyFlutterReader -typedef NS_ENUM(UInt8, _Value) { - _valueClientOptions = 128, - _valueTokenDetails = 129, - _valueAblyMessage = 255, -}; - -(id)readValueOfType:(const UInt8)type { switch ((_Value)type) { case _valueClientOptions: diff --git a/ios/Classes/AblyFlutterReaderWriter.h b/ios/Classes/codec/AblyFlutterReaderWriter.h similarity index 100% rename from ios/Classes/AblyFlutterReaderWriter.h rename to ios/Classes/codec/AblyFlutterReaderWriter.h diff --git a/ios/Classes/AblyFlutterReaderWriter.m b/ios/Classes/codec/AblyFlutterReaderWriter.m similarity index 60% rename from ios/Classes/AblyFlutterReaderWriter.m rename to ios/Classes/codec/AblyFlutterReaderWriter.m index f3c0a8d1c..b40ec419e 100644 --- a/ios/Classes/AblyFlutterReaderWriter.m +++ b/ios/Classes/codec/AblyFlutterReaderWriter.m @@ -1,5 +1,6 @@ #import "AblyFlutterReaderWriter.h" #import "AblyFlutterReader.h" +#import "AblyFlutterWriter.h" @implementation AblyFlutterReaderWriter @@ -7,4 +8,8 @@ -(FlutterStandardReader *)readerWithData:(NSData *const)data { return [[AblyFlutterReader alloc] initWithData:data]; } +- (FlutterStandardWriter*)writerWithData:(NSMutableData*)data { + return [[AblyFlutterWriter alloc] initWithData:data]; +} + @end diff --git a/ios/Classes/AblyFlutterReader.h b/ios/Classes/codec/AblyFlutterWriter.h similarity index 63% rename from ios/Classes/AblyFlutterReader.h rename to ios/Classes/codec/AblyFlutterWriter.h index a51bede1d..1b179626e 100644 --- a/ios/Classes/AblyFlutterReader.h +++ b/ios/Classes/codec/AblyFlutterWriter.h @@ -3,7 +3,7 @@ NS_ASSUME_NONNULL_BEGIN -@interface AblyFlutterReader : FlutterStandardReader +@interface AblyFlutterWriter : FlutterStandardWriter @end NS_ASSUME_NONNULL_END diff --git a/ios/Classes/codec/AblyFlutterWriter.m b/ios/Classes/codec/AblyFlutterWriter.m new file mode 100644 index 000000000..a43dc1d63 --- /dev/null +++ b/ios/Classes/codec/AblyFlutterWriter.m @@ -0,0 +1,28 @@ +#import "AblyFlutterWriter.h" +#import "Ably.h" +#import "AblyFlutterMessage.h" +#import "AblyFlutterReader.h" + + +@implementation AblyFlutterWriter + +- (void)writeValue:(id)value { + if([value isKindOfClass:[ARTErrorInfo class]]){ + [self writeByte:_ValueErrorInfo]; + [self writeErrorInfo: value]; + return; + } + + [super writeValue:value]; +} + +- (void) writeErrorInfo:(ARTErrorInfo *) e{ + [self writeValue: nil]; //code - not available in ably-cocoa + [self writeValue: [e message]]; + [self writeValue: @([e statusCode])]; + [self writeValue: nil]; //href - not available in ably-cocoa + [self writeValue: nil]; //requestId - not available in ably-java + [self writeValue: nil]; //cause - not available in ably-java +} + +@end diff --git a/lib/src/codec.dart b/lib/src/codec.dart index a73664709..6ca8812e1 100644 --- a/lib/src/codec.dart +++ b/lib/src/codec.dart @@ -5,9 +5,28 @@ import 'package:flutter/services.dart'; import '../ably.dart'; -class Codec extends StandardMessageCodec { +typedef CodecEncoder(final WriteBuffer buffer, final dynamic value); +typedef T CodecDecoder(ReadBuffer buffer); +class CodecPair{ + + final Function encoder; + final CodecDecoder decoder; + CodecPair(this.encoder, this.decoder); + + encode(final WriteBuffer buffer, final dynamic value){ + if(this.encoder==null) throw AblyException("Codec encoder not defined"); + return this.encoder(buffer, value); + } + + decode(ReadBuffer buffer){ + if(this.decoder==null) throw AblyException("Codec decoder not defined"); + return this.decoder(buffer); + } + +} - const Codec(); + +class Codec extends StandardMessageCodec { // Custom type values must be over 127. At the time of writing the standard message // codec encodes them as an unsigned byte which means the maximum type value is 255. @@ -19,126 +38,177 @@ class Codec extends StandardMessageCodec { // https://api.flutter.dev/flutter/services/StandardMessageCodec/writeValue.html static const _valueClientOptions = 128; static const _valueTokenDetails = 129; + static const _valueErrorInfo = 144; static const _valueAblyMessage = 255; - @override - void writeValue (final WriteBuffer buffer, final dynamic value) { + Map codecMap; + + Codec():super(){ + this.codecMap = { + _valueClientOptions: CodecPair(encodeClientOptions, decodeClientOptions), + _valueTokenDetails: CodecPair(encodeTokenDetails, decodeTokenDetails), + _valueErrorInfo: CodecPair(null, decodeErrorInfo), + _valueAblyMessage: CodecPair(encodeAblyMessage, decodeAblyMessage), + }; + } + + getCodecType(final dynamic value){ if (value is ClientOptions) { - buffer.putUint8(_valueClientOptions); - final ClientOptions v = value; - - // AuthOptions (super class of ClientOptions) - writeValue(buffer, v.authUrl); - writeValue(buffer, v.authMethod); - writeValue(buffer, v.key); - writeValue(buffer, v.tokenDetails); - writeValue(buffer, v.authHeaders); - writeValue(buffer, v.authParams); - writeValue(buffer, v.queryTime); - writeValue(buffer, v.useTokenAuth); - - // ClientOptions - writeValue(buffer, v.clientId); - writeValue(buffer, v.logLevel); - //TODO handle logHandler - writeValue(buffer, v.tls); - writeValue(buffer, v.restHost); - writeValue(buffer, v.realtimeHost); - writeValue(buffer, v.port); - writeValue(buffer, v.tlsPort); - writeValue(buffer, v.autoConnect); - writeValue(buffer, v.useBinaryProtocol); - writeValue(buffer, v.queueMessages); - writeValue(buffer, v.echoMessages); - writeValue(buffer, v.recover); - writeValue(buffer, v.environment); - writeValue(buffer, v.idempotentRestPublishing); - writeValue(buffer, v.httpOpenTimeout); - writeValue(buffer, v.httpRequestTimeout); - writeValue(buffer, v.httpMaxRetryCount); - writeValue(buffer, v.realtimeRequestTimeout); - writeValue(buffer, v.fallbackHosts); - writeValue(buffer, v.fallbackHostsUseDefault); - writeValue(buffer, v.fallbackRetryTimeout); - writeValue(buffer, v.defaultTokenParams); - writeValue(buffer, v.channelRetryTimeout); - writeValue(buffer, v.transportParams); + return _valueClientOptions; } else if (value is TokenDetails) { - buffer.putUint8(_valueTokenDetails); - final TokenDetails v = value; - writeValue(buffer, v.token); - writeValue(buffer, v.expires); - writeValue(buffer, v.issued); - writeValue(buffer, v.capability); - writeValue(buffer, v.clientId); + return _valueTokenDetails; + } else if (value is ErrorInfo) { + return _valueErrorInfo; } else if (value is AblyMessage) { - buffer.putUint8(_valueAblyMessage); - final AblyMessage v = value; - writeValue(buffer, v.registrationHandle); - writeValue(buffer, v.message); - } else { + return _valueAblyMessage; + } + } + + @override + void writeValue (final WriteBuffer buffer, final dynamic value) { + int type = getCodecType(value); + if(type==null){ super.writeValue(buffer, value); + }else { + buffer.putUint8(type); + codecMap[type].encode(buffer, value); } } dynamic readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case _valueClientOptions: - final ClientOptions v = ClientOptions(); - - // AuthOptions (super class of ClientOptions) - v.authUrl = readValue(buffer) as String; - v.authMethod = readValue(buffer) as HTTPMethods; - v.key = readValue(buffer) as String; - v.tokenDetails = readValue(buffer) as TokenDetails; - v.authHeaders = readValue(buffer) as Map; - v.authParams = readValue(buffer) as Map; - v.queryTime = readValue(buffer) as bool; - v.useTokenAuth = readValue(buffer) as bool; - - // ClientOptions - v.clientId = readValue(buffer) as String; - v.logLevel = readValue(buffer) as int; - //TODO handle logHandler - v.tls = readValue(buffer) as bool; - v.restHost = readValue(buffer) as String; - v.realtimeHost = readValue(buffer) as String; - v.port = readValue(buffer) as int; - v.tlsPort = readValue(buffer) as int; - v.autoConnect = readValue(buffer) as bool; - v.useBinaryProtocol = readValue(buffer) as bool; - v.queueMessages = readValue(buffer) as bool; - v.echoMessages = readValue(buffer) as bool; - v.recover = readValue(buffer) as String; - v.environment = readValue(buffer) as String; - v.idempotentRestPublishing = readValue(buffer) as bool; - v.httpOpenTimeout = readValue(buffer) as int; - v.httpRequestTimeout = readValue(buffer) as int; - v.httpMaxRetryCount = readValue(buffer) as int; - v.realtimeRequestTimeout = readValue(buffer) as int; - v.fallbackHosts = readValue(buffer) as List; - v.fallbackHostsUseDefault = readValue(buffer) as bool; - v.fallbackRetryTimeout = readValue(buffer) as int; - v.defaultTokenParams = readValue(buffer) as TokenParams; - v.channelRetryTimeout = readValue(buffer) as int; - v.transportParams = readValue(buffer) as Map; - return v; - - case _valueTokenDetails: - TokenDetails t = TokenDetails(readValue(buffer)); - t.expires = readValue(buffer) as int; - t.issued = readValue(buffer) as int; - t.capability = readValue(buffer) as String; - t.clientId = readValue(buffer) as String; - return t; - case _valueAblyMessage: - return AblyMessage( - readValue(buffer) as int, - readValue(buffer), - ); - default: - return super.readValueOfType(type, buffer); + CodecPair pair = codecMap[type]; + if(pair==null){ + return super.readValueOfType(type, buffer); + }else{ + return pair.decode(buffer); } } + encodeClientOptions(final WriteBuffer buffer, final dynamic value){ + final ClientOptions v = value; + + // AuthOptions (super class of ClientOptions) + writeValue(buffer, v.authUrl); + writeValue(buffer, v.authMethod); + writeValue(buffer, v.key); + writeValue(buffer, v.tokenDetails); + writeValue(buffer, v.authHeaders); + writeValue(buffer, v.authParams); + writeValue(buffer, v.queryTime); + writeValue(buffer, v.useTokenAuth); + + // ClientOptions + writeValue(buffer, v.clientId); + writeValue(buffer, v.logLevel); + //TODO handle logHandler + writeValue(buffer, v.tls); + writeValue(buffer, v.restHost); + writeValue(buffer, v.realtimeHost); + writeValue(buffer, v.port); + writeValue(buffer, v.tlsPort); + writeValue(buffer, v.autoConnect); + writeValue(buffer, v.useBinaryProtocol); + writeValue(buffer, v.queueMessages); + writeValue(buffer, v.echoMessages); + writeValue(buffer, v.recover); + writeValue(buffer, v.environment); + writeValue(buffer, v.idempotentRestPublishing); + writeValue(buffer, v.httpOpenTimeout); + writeValue(buffer, v.httpRequestTimeout); + writeValue(buffer, v.httpMaxRetryCount); + writeValue(buffer, v.realtimeRequestTimeout); + writeValue(buffer, v.fallbackHosts); + writeValue(buffer, v.fallbackHostsUseDefault); + writeValue(buffer, v.fallbackRetryTimeout); + writeValue(buffer, v.defaultTokenParams); + writeValue(buffer, v.channelRetryTimeout); + writeValue(buffer, v.transportParams); + } + + encodeTokenDetails(final WriteBuffer buffer, final dynamic value){ + final TokenDetails v = value; + writeValue(buffer, v.token); + writeValue(buffer, v.expires); + writeValue(buffer, v.issued); + writeValue(buffer, v.capability); + writeValue(buffer, v.clientId); + } + + encodeAblyMessage(final WriteBuffer buffer, final dynamic value){ + final AblyMessage v = value; + writeValue(buffer, v.registrationHandle); + writeValue(buffer, v.message); + } + + + // =========== DECODERS =========== + ClientOptions decodeClientOptions(ReadBuffer buffer){ + final ClientOptions v = ClientOptions(); + + // AuthOptions (super class of ClientOptions) + v.authUrl = readValue(buffer) as String; + v.authMethod = readValue(buffer) as HTTPMethods; + v.key = readValue(buffer) as String; + v.tokenDetails = readValue(buffer) as TokenDetails; + v.authHeaders = readValue(buffer) as Map; + v.authParams = readValue(buffer) as Map; + v.queryTime = readValue(buffer) as bool; + v.useTokenAuth = readValue(buffer) as bool; + + // ClientOptions + v.clientId = readValue(buffer) as String; + v.logLevel = readValue(buffer) as int; + //TODO handle logHandler + v.tls = readValue(buffer) as bool; + v.restHost = readValue(buffer) as String; + v.realtimeHost = readValue(buffer) as String; + v.port = readValue(buffer) as int; + v.tlsPort = readValue(buffer) as int; + v.autoConnect = readValue(buffer) as bool; + v.useBinaryProtocol = readValue(buffer) as bool; + v.queueMessages = readValue(buffer) as bool; + v.echoMessages = readValue(buffer) as bool; + v.recover = readValue(buffer) as String; + v.environment = readValue(buffer) as String; + v.idempotentRestPublishing = readValue(buffer) as bool; + v.httpOpenTimeout = readValue(buffer) as int; + v.httpRequestTimeout = readValue(buffer) as int; + v.httpMaxRetryCount = readValue(buffer) as int; + v.realtimeRequestTimeout = readValue(buffer) as int; + v.fallbackHosts = readValue(buffer) as List; + v.fallbackHostsUseDefault = readValue(buffer) as bool; + v.fallbackRetryTimeout = readValue(buffer) as int; + v.defaultTokenParams = readValue(buffer) as TokenParams; + v.channelRetryTimeout = readValue(buffer) as int; + v.transportParams = readValue(buffer) as Map; + return v; + } + + TokenDetails decodeTokenDetails(ReadBuffer buffer){ + TokenDetails t = TokenDetails(readValue(buffer)); + t.expires = readValue(buffer) as int; + t.issued = readValue(buffer) as int; + t.capability = readValue(buffer) as String; + t.clientId = readValue(buffer) as String; + return t; + } + + AblyMessage decodeAblyMessage(ReadBuffer buffer){ + return AblyMessage( + readValue(buffer) as int, + readValue(buffer), + ); + } + + ErrorInfo decodeErrorInfo(ReadBuffer buffer){ + return ErrorInfo( + code: readValue(buffer) as int, + message: readValue(buffer) as String, + statusCode: readValue(buffer) as int, + href: readValue(buffer) as String, + requestId: readValue(buffer) as String, + cause: readValue(buffer) as ErrorInfo + ); + } + } diff --git a/lib/src/impl/rest/channels.dart b/lib/src/impl/rest/channels.dart index 6fd71e4dd..ade305586 100644 --- a/lib/src/impl/rest/channels.dart +++ b/lib/src/impl/rest/channels.dart @@ -36,7 +36,7 @@ class RestPlatformChannel extends PlatformObject implements spec.Channel{ if(data!=null) _map["message"] = data; await this.invoke(PlatformMethod.publish, _map); } on PlatformException catch (pe) { - throw spec.AblyException(pe.code, pe.message); + throw spec.AblyException(pe.code, pe.message, pe.details); } } diff --git a/lib/src/spec/common.dart b/lib/src/spec/common.dart index e86c2183a..564638e0a 100644 --- a/lib/src/spec/common.dart +++ b/lib/src/spec/common.dart @@ -12,13 +12,29 @@ abstract class CipherParams { String mode; } -abstract class ErrorInfo { - int code; - String herf; - String message; - ErrorInfo cause; - int statusCode; - String requestId; +///An [AblyException] encapsulates [ErrorInfo] which carries details +///about information related to Ably-specific error [code], +/// generic [statusCode], error [message], +/// link to error related documentation as [href], +/// [requestId] and [cause] of this exception +class ErrorInfo { + final int code; + final String href; + final String message; + final ErrorInfo cause; + final int statusCode; + final String requestId; + + ErrorInfo({ + this.code, this.href, this.message, + this.cause, this.statusCode, this.requestId + }); + + @override + String toString() { + return 'ErrorInfo message=$message code=$code statusCode=$statusCode href=$href'; + } + } abstract class StatsMessageCount { @@ -120,8 +136,9 @@ class AblyException implements Exception { final String code; final String message; + final ErrorInfo errorInfo; - AblyException([this.code, this.message]); + AblyException([this.code, this.message, this.errorInfo]); String toString() { if (message == null) return "AblyException";