diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index afecc10659..2c96dc7816 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.20 + +* Implemented `@async` HostApi's for iOS. +* Fixed async FlutterApi methods with void return. + ## 0.1.19 * Fixed a bug introduced in 0.1.17 where methods without arguments were diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart index e3c3a96925..747a7ed56d 100644 --- a/packages/pigeon/lib/ast.dart +++ b/packages/pigeon/lib/ast.dart @@ -17,7 +17,8 @@ class Node {} /// Represents a method on an [Api]. class Method extends Node { /// Parametric constructor for [Method]. - Method({this.name, this.returnType, this.argType, this.isAsynchronous}); + Method( + {this.name, this.returnType, this.argType, this.isAsynchronous = false}); /// The name of the method. String name; diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart index 0e948a2eaa..996e794a08 100644 --- a/packages/pigeon/lib/generator_tools.dart +++ b/packages/pigeon/lib/generator_tools.dart @@ -8,7 +8,7 @@ import 'dart:mirrors'; import 'ast.dart'; /// The current version of pigeon. This must match the version in pubspec.yaml. -const String pigeonVersion = '0.1.19'; +const String pigeonVersion = '0.1.20'; /// Read all the content from [stdin] to a String. String readStdin() { @@ -72,9 +72,7 @@ class Indent { if (begin != null) { _sink.write(begin + newline); } - inc(); - func(); - dec(); + nest(1, func); if (end != null) { _sink.write(str() + end); if (addTrailingNewline) { @@ -83,6 +81,17 @@ class Indent { } } + /// Like `scoped` but writes the current indentation level. + void writeScoped( + String begin, + String end, + Function func, { + bool addTrailingNewline = true, + }) { + scoped(str() + begin ?? '', end, func, + addTrailingNewline: addTrailingNewline); + } + /// Scoped increase of the ident level. For the execution of [func] the /// indentation will be incremented by the given amount. void nest(int count, Function func) { diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart index e264ef35e2..a0ce4ba9c2 100644 --- a/packages/pigeon/lib/objc_generator.dart +++ b/packages/pigeon/lib/objc_generator.dart @@ -71,6 +71,91 @@ String _propertyTypeForDartType(String type) { } } +void _writeClassDeclarations( + Indent indent, List classes, String prefix) { + for (Class klass in classes) { + indent.writeln('@interface ${_className(prefix, klass.name)} : NSObject'); + for (Field field in klass.fields) { + final HostDatatype hostDatatype = getHostDatatype( + field, classes, _objcTypeForDartType, + customResolver: (String x) => '${_className(prefix, x)} *'); + final String propertyType = hostDatatype.isBuiltin + ? _propertyTypeForDartType(field.dataType) + : 'strong'; + final String nullability = + hostDatatype.datatype.contains('*') ? ', nullable' : ''; + indent.writeln( + '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};'); + } + indent.writeln('@end'); + indent.writeln(''); + } +} + +void _writeHostApiDeclaration(Indent indent, Api api, ObjcOptions options) { + final String apiName = _className(options.prefix, api.name); + indent.writeln('@protocol $apiName'); + for (Method func in api.methods) { + final String returnTypeName = _className(options.prefix, func.returnType); + if (func.isAsynchronous) { + if (func.returnType == 'void') { + if (func.argType == 'void') { + indent.writeln( + '-(void)${func.name}:(void(^)(FlutterError *_Nullable))completion;'); + } else { + final String argType = _className(options.prefix, func.argType); + indent.writeln( + '-(void)${func.name}:(nullable $argType *)input completion:(void(^)(FlutterError *_Nullable))completion;'); + } + } else { + if (func.argType == 'void') { + indent.writeln( + '-(void)${func.name}:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;'); + } else { + final String argType = _className(options.prefix, func.argType); + indent.writeln( + '-(void)${func.name}:(nullable $argType *)input completion:(void(^)($returnTypeName *_Nullable, FlutterError *_Nullable))completion;'); + } + } + } else { + final String returnType = + func.returnType == 'void' ? 'void' : 'nullable $returnTypeName *'; + if (func.argType == 'void') { + indent.writeln( + '-($returnType)${func.name}:(FlutterError *_Nullable *_Nonnull)error;'); + } else { + final String argType = _className(options.prefix, func.argType); + indent.writeln( + '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;'); + } + } + } + indent.writeln('@end'); + indent.writeln(''); + indent.writeln( + 'extern void ${apiName}Setup(id binaryMessenger, id<$apiName> _Nullable api);'); + indent.writeln(''); +} + +void _writeFlutterApiDeclaration(Indent indent, Api api, ObjcOptions options) { + final String apiName = _className(options.prefix, api.name); + indent.writeln('@interface $apiName : NSObject'); + indent.writeln( + '- (instancetype)initWithBinaryMessenger:(id)binaryMessenger;'); + for (Method func in api.methods) { + final String returnType = _className(options.prefix, func.returnType); + final String callbackType = _callbackForType(func.returnType, returnType); + if (func.argType == 'void') { + indent.writeln('- (void)${func.name}:($callbackType)completion;'); + } else { + final String argType = _className(options.prefix, func.argType); + indent.writeln( + '- (void)${func.name}:($argType*)input completion:($callbackType)completion;'); + } + } + indent.writeln('@end'); +} + /// Generates the ".h" file for the AST represented by [root] to [sink] with the /// provided [options]. void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) { @@ -92,65 +177,13 @@ void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) { indent.writeln(''); - for (Class klass in root.classes) { - indent.writeln( - '@interface ${_className(options.prefix, klass.name)} : NSObject'); - for (Field field in klass.fields) { - final HostDatatype hostDatatype = getHostDatatype( - field, root.classes, _objcTypeForDartType, - customResolver: (String x) => '${_className(options.prefix, x)} *'); - final String propertyType = hostDatatype.isBuiltin - ? _propertyTypeForDartType(field.dataType) - : 'strong'; - final String nullability = - hostDatatype.datatype.contains('*') ? ', nullable' : ''; - indent.writeln( - '@property(nonatomic, $propertyType$nullability) ${hostDatatype.datatype} ${field.name};'); - } - indent.writeln('@end'); - indent.writeln(''); - } + _writeClassDeclarations(indent, root.classes, options.prefix); for (Api api in root.apis) { - final String apiName = _className(options.prefix, api.name); if (api.location == ApiLocation.host) { - indent.writeln('@protocol $apiName'); - for (Method func in api.methods) { - final String returnTypeName = - _className(options.prefix, func.returnType); - final String returnType = - func.returnType == 'void' ? 'void' : 'nullable $returnTypeName *'; - if (func.argType == 'void') { - indent.writeln( - '-($returnType)${func.name}:(FlutterError *_Nullable *_Nonnull)error;'); - } else { - final String argType = _className(options.prefix, func.argType); - indent.writeln( - '-($returnType)${func.name}:($argType*)input error:(FlutterError *_Nullable *_Nonnull)error;'); - } - } - indent.writeln('@end'); - indent.writeln(''); - indent.writeln( - 'extern void ${apiName}Setup(id binaryMessenger, id<$apiName> _Nullable api);'); - indent.writeln(''); + _writeHostApiDeclaration(indent, api, options); } else if (api.location == ApiLocation.flutter) { - indent.writeln('@interface $apiName : NSObject'); - indent.writeln( - '- (instancetype)initWithBinaryMessenger:(id)binaryMessenger;'); - for (Method func in api.methods) { - final String returnType = _className(options.prefix, func.returnType); - final String callbackType = - _callbackForType(func.returnType, returnType); - if (func.argType == 'void') { - indent.writeln('- (void)${func.name}:($callbackType)completion;'); - } else { - final String argType = _className(options.prefix, func.argType); - indent.writeln( - '- (void)${func.name}:($argType*)input completion:($callbackType)completion;'); - } - } - indent.writeln('@end'); + _writeFlutterApiDeclaration(indent, api, options); } } @@ -204,21 +237,56 @@ void _writeHostApiSource(Indent indent, ObjcOptions options, Api api) { indent.scoped('{', '}];', () { final String returnType = _className(options.prefix, func.returnType); - indent.writeln('FlutterError *error;'); - String call; + String syncCall; if (func.argType == 'void') { - call = '[api ${func.name}:&error]'; + syncCall = '[api ${func.name}:&error]'; } else { final String argType = _className(options.prefix, func.argType); indent.writeln('$argType *input = [$argType fromMap:message];'); - call = '[api ${func.name}:input error:&error]'; + syncCall = '[api ${func.name}:input error:&error]'; } - if (func.returnType == 'void') { - indent.writeln('$call;'); - indent.writeln('callback(wrapResult(nil, error));'); + if (func.isAsynchronous) { + if (func.returnType == 'void') { + const String callback = 'callback(error));'; + if (func.argType == 'void') { + indent.writeScoped( + '[api ${func.name}:^(FlutterError *_Nullable error) {', + '}];', () { + indent.writeln(callback); + }); + } else { + indent.writeScoped( + '[api ${func.name}:input completion:^(FlutterError *_Nullable error) {', + '}];', () { + indent.writeln(callback); + }); + } + } else { + const String callback = + 'callback(wrapResult([output toMap], error));'; + if (func.argType == 'void') { + indent.writeScoped( + '[api ${func.name}:^($returnType *_Nullable output, FlutterError *_Nullable error) {', + '}];', () { + indent.writeln(callback); + }); + } else { + indent.writeScoped( + '[api ${func.name}:input completion:^($returnType *_Nullable output, FlutterError *_Nullable error) {', + '}];', () { + indent.writeln(callback); + }); + } + } } else { - indent.writeln('$returnType *output = $call;'); - indent.writeln('callback(wrapResult([output toMap], error));'); + indent.writeln('FlutterError *error;'); + if (func.returnType == 'void') { + indent.writeln('$syncCall;'); + indent.writeln('callback(wrapResult(nil, error));'); + } else { + indent.writeln('$returnType *output = $syncCall;'); + indent.writeln('callback(wrapResult([output toMap], error));'); + } } }); }); @@ -246,7 +314,7 @@ void _writeFlutterApiSource(Indent indent, ObjcOptions options, Api api) { indent.writeln('self = [super init];'); indent.write('if (self) '); indent.scoped('{', '}', () { - indent.writeln('self.binaryMessenger = binaryMessenger;'); + indent.writeln('_binaryMessenger = binaryMessenger;'); }); indent.writeln('return self;'); }); @@ -312,19 +380,19 @@ void generateObjcSource(ObjcOptions options, Root root, StringSink sink) { indent.addln(''); indent.format( - '''static NSDictionary* wrapResult(NSDictionary *result, FlutterError *error) { + '''static NSDictionary* wrapResult(NSDictionary *result, FlutterError *error) { \tNSDictionary *errorDict = (NSDictionary *)[NSNull null]; \tif (error) { -\t\terrorDict = [NSDictionary dictionaryWithObjectsAndKeys: -\t\t\t\t(error.code ? error.code : [NSNull null]), @"${Keys.errorCode}", -\t\t\t\t(error.message ? error.message : [NSNull null]), @"${Keys.errorMessage}", -\t\t\t\t(error.details ? error.details : [NSNull null]), @"${Keys.errorDetails}", -\t\t\t\tnil]; +\t\terrorDict = @{ +\t\t\t\t@"${Keys.errorCode}": (error.code ? error.code : [NSNull null]), +\t\t\t\t@"${Keys.errorMessage}": (error.message ? error.message : [NSNull null]), +\t\t\t\t@"${Keys.errorDetails}": (error.details ? error.details : [NSNull null]), +\t\t\t\t}; \t} -\treturn [NSDictionary dictionaryWithObjectsAndKeys: -\t\t\t(result ? result : [NSNull null]), @"${Keys.result}", -\t\t\terrorDict, @"${Keys.error}", -\t\t\tnil]; +\treturn @{ +\t\t\t@"${Keys.result}": (result ? result : [NSNull null]), +\t\t\t@"${Keys.error}": errorDict, +\t\t\t}; }'''); indent.addln(''); diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj index 20ab37a48e..cca797a82d 100644 --- a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 0D50126D23FF759100CD5B95 /* messages.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50126B23FF759100CD5B95 /* messages.m */; }; 0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D50127423FF75B100CD5B95 /* RunnerTests.m */; }; + 0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35E725D45A3000B76435 /* async_handlers.m */; }; + 0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -47,6 +49,9 @@ 0D50127223FF75B100CD5B95 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 0D50127423FF75B100CD5B95 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; 0D50127623FF75B100CD5B95 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0D8C35E625D45A3000B76435 /* async_handlers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_handlers.h; sourceTree = ""; }; + 0D8C35E725D45A3000B76435 /* async_handlers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = async_handlers.m; sourceTree = ""; }; + 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AsyncHandlersTest.m; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -86,6 +91,7 @@ children = ( 0D50127423FF75B100CD5B95 /* RunnerTests.m */, 0D50127623FF75B100CD5B95 /* Info.plist */, + 0D8C35EA25D45A7900B76435 /* AsyncHandlersTest.m */, ); path = RunnerTests; sourceTree = ""; @@ -123,6 +129,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 0D8C35E625D45A3000B76435 /* async_handlers.h */, + 0D8C35E725D45A3000B76435 /* async_handlers.m */, 0D50126C23FF759100CD5B95 /* messages.h */, 0D50126B23FF759100CD5B95 /* messages.m */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, @@ -283,6 +291,7 @@ buildActionMask = 2147483647; files = ( 0D50127523FF75B100CD5B95 /* RunnerTests.m in Sources */, + 0D8C35EB25D45A7900B76435 /* AsyncHandlersTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -294,6 +303,7 @@ 0D50126D23FF759100CD5B95 /* messages.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 0D8C35E825D45A3000B76435 /* async_handlers.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h new file mode 100644 index 0000000000..a69bdb6a97 --- /dev/null +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.h @@ -0,0 +1,27 @@ +// Autogenerated from Pigeon (v0.1.20), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +@class Value; + +@interface Value : NSObject +@property(nonatomic, strong, nullable) NSNumber *number; +@end + +@interface Api2Flutter : NSObject +- (instancetype)initWithBinaryMessenger:(id)binaryMessenger; +- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion; +@end +@protocol Api2Host +- (void)calculate:(nullable Value *)input + completion:(void (^)(Value *_Nullable, FlutterError *_Nullable))completion; +@end + +extern void Api2HostSetup(id binaryMessenger, id _Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m new file mode 100644 index 0000000000..73f062e979 --- /dev/null +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/Runner/async_handlers.m @@ -0,0 +1,88 @@ +// Autogenerated from Pigeon (v0.1.20), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "async_handlers.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; + } + return @{ + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; +} + +@interface Value () ++ (Value *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation Value ++ (Value *)fromMap:(NSDictionary *)dict { + Value *result = [[Value alloc] init]; + result.number = dict[@"number"]; + if ((NSNull *)result.number == [NSNull null]) { + result.number = nil; + } + return result; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.number ? self.number : [NSNull null]), @"number", nil]; +} +@end + +@interface Api2Flutter () +@property(nonatomic, strong) NSObject *binaryMessenger; +@end + +@implementation Api2Flutter +- (instancetype)initWithBinaryMessenger:(NSObject *)binaryMessenger { + self = [super init]; + if (self) { + _binaryMessenger = binaryMessenger; + } + return self; +} + +- (void)calculate:(Value *)input completion:(void (^)(Value *, NSError *_Nullable))completion { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Flutter.calculate" + binaryMessenger:self.binaryMessenger]; + NSDictionary *inputMap = [input toMap]; + [channel sendMessage:inputMap + reply:^(id reply) { + NSDictionary *outputMap = reply; + Value *output = [Value fromMap:outputMap]; + completion(output, nil); + }]; +} +@end +void Api2HostSetup(id binaryMessenger, id api) { + { + FlutterBasicMessageChannel *channel = + [FlutterBasicMessageChannel messageChannelWithName:@"dev.flutter.pigeon.Api2Host.calculate" + binaryMessenger:binaryMessenger]; + if (api) { + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + Value *input = [Value fromMap:message]; + [api calculate:input + completion:^(Value *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult([output toMap], error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m new file mode 100644 index 0000000000..f32e04697d --- /dev/null +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/AsyncHandlersTest.m @@ -0,0 +1,139 @@ +#import +#import +#import "async_handlers.h" + +/////////////////////////////////////////////////////////////////////////////////////////// +@interface Value () ++ (Value*)fromMap:(NSDictionary*)dict; +- (NSDictionary*)toMap; +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@interface MockBinaryMessenger : NSObject +@property(nonatomic, copy) NSNumber* result; +@property(nonatomic, retain) FlutterStandardMessageCodec* codec; +@property(nonatomic, retain) NSMutableDictionary* handlers; +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@implementation MockBinaryMessenger + +- (instancetype)init { + self = [super init]; + if (self) { + _codec = [FlutterStandardMessageCodec sharedInstance]; + _handlers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection { +} + +- (void)sendOnChannel:(nonnull NSString*)channel message:(NSData* _Nullable)message { +} + +- (void)sendOnChannel:(nonnull NSString*)channel + message:(NSData* _Nullable)message + binaryReply:(FlutterBinaryReply _Nullable)callback { + if (self.result) { + Value* output = [[Value alloc] init]; + output.number = self.result; + NSDictionary* outputDictionary = [output toMap]; + callback([_codec encode:outputDictionary]); + } +} + +- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString*)channel + binaryMessageHandler: + (FlutterBinaryMessageHandler _Nullable)handler { + _handlers[channel] = [handler copy]; + return _handlers.count; +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@interface MockApi2Host : NSObject +@property(nonatomic, copy) NSNumber* output; +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@implementation MockApi2Host + +- (void)calculate:(Value* _Nullable)input + completion:(nonnull void (^)(Value* _Nullable, FlutterError* _Nullable))completion { + if (self.output) { + Value* output = [[Value alloc] init]; + output.number = self.output; + completion(output, nil); + } else { + completion(nil, [FlutterError errorWithCode:@"hey" message:@"ho" details:nil]); + } +} + +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@interface AsyncHandlersTest : XCTestCase +@end + +/////////////////////////////////////////////////////////////////////////////////////////// +@implementation AsyncHandlersTest + +- (void)testAsyncHost2Flutter { + MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init]; + binaryMessenger.result = @(2); + Api2Flutter* api2Flutter = [[Api2Flutter alloc] initWithBinaryMessenger:binaryMessenger]; + Value* input = [[Value alloc] init]; + input.number = @(1); + XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"]; + [api2Flutter calculate:input + completion:^(Value* _Nonnull output, NSError* _Nullable error) { + XCTAssertEqual(output.number.intValue, 2); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testAsyncFlutter2Host { + MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init]; + MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init]; + mockApi2Host.output = @(2); + Api2HostSetup(binaryMessenger, mockApi2Host); + NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate"; + XCTAssertNotNil(binaryMessenger.handlers[channelName]); + + Value* input = [[Value alloc] init]; + input.number = @(1); + NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]]; + XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"]; + binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) { + NSDictionary* outputMap = [binaryMessenger.codec decode:data]; + Value* output = [Value fromMap:outputMap[@"result"]]; + XCTAssertEqual(output.number.intValue, 2); + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +- (void)testAsyncFlutter2HostError { + MockBinaryMessenger* binaryMessenger = [[MockBinaryMessenger alloc] init]; + MockApi2Host* mockApi2Host = [[MockApi2Host alloc] init]; + Api2HostSetup(binaryMessenger, mockApi2Host); + NSString* channelName = @"dev.flutter.pigeon.Api2Host.calculate"; + XCTAssertNotNil(binaryMessenger.handlers[channelName]); + + Value* input = [[Value alloc] init]; + input.number = @(1); + NSData* inputEncoded = [binaryMessenger.codec encode:[input toMap]]; + XCTestExpectation* expectation = [self expectationWithDescription:@"calculate callback"]; + binaryMessenger.handlers[channelName](inputEncoded, ^(NSData* data) { + NSDictionary* outputMap = [binaryMessenger.codec decode:data]; + XCTAssertNotNil(outputMap[@"error"]); + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1.0 handler:nil]; +} + +@end diff --git a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m index 926b7ebc92..f077fd464a 100644 --- a/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m +++ b/packages/pigeon/platform_tests/ios_unit_tests/ios/RunnerTests/RunnerTests.m @@ -1,11 +1,3 @@ -// -// RunnerTests.m -// RunnerTests -// -// Created by Aaron Clarke on 2/20/20. -// Copyright © 2020 The Chromium Authors. All rights reserved. -// - #import #import "messages.h" diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 1fd88de9f9..408779eaad 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -1,5 +1,5 @@ name: pigeon -version: 0.1.19 # This must match the version in lib/generator_tools.dart +version: 0.1.20 # This must match the version in lib/generator_tools.dart description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. homepage: https://github.com/flutter/packages/tree/master/packages/pigeon dependencies: diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh index f45d79ccb5..c6b5b20c52 100755 --- a/packages/pigeon/run_tests.sh +++ b/packages/pigeon/run_tests.sh @@ -134,6 +134,7 @@ pushd $PWD cd e2e_tests/test_objc/ flutter pub get popd +test_pigeon_ios ./pigeons/async_handlers.dart test_null_safe_dart ./pigeons/message.dart test_pigeon_android ./pigeons/voidflutter.dart test_pigeon_android ./pigeons/voidhost.dart @@ -152,8 +153,6 @@ test_pigeon_ios ./pigeons/void_arg_host.dart test_pigeon_ios ./pigeons/void_arg_flutter.dart test_pigeon_ios ./pigeons/list.dart test_pigeon_ios ./pigeons/all_datatypes.dart -# Not implemented yet. -# test_pigeon_ios ./pigeons/async_handlers.dart ############################################################################### # iOS unit tests on generated code. @@ -163,8 +162,15 @@ pub run pigeon \ --dart_out /dev/null \ --objc_header_out platform_tests/ios_unit_tests/ios/Runner/messages.h \ --objc_source_out platform_tests/ios_unit_tests/ios/Runner/messages.m +pub run pigeon \ + --input pigeons/async_handlers.dart \ + --dart_out /dev/null \ + --objc_header_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.h \ + --objc_source_out platform_tests/ios_unit_tests/ios/Runner/async_handlers.m clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.h clang-format -i platform_tests/ios_unit_tests/ios/Runner/messages.m +clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.h +clang-format -i platform_tests/ios_unit_tests/ios/Runner/async_handlers.m pushd $PWD cd platform_tests/ios_unit_tests flutter build ios --simulator diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart index 1acbcc5680..041d811354 100644 --- a/packages/pigeon/test/objc_generator_test.dart +++ b/packages/pigeon/test/objc_generator_test.dart @@ -431,4 +431,190 @@ void main() { expect(code, contains('@interface Foobar')); expect(code, matches('@property.*NSDictionary.*field1')); }); + + test('async void(input) HostApi header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'Input', + returnType: 'void', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(FlutterError *_Nullable))completion')); + }); + + test('async output(input) HostApi header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'Input', + returnType: 'Output', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '(void)doSomething:(nullable ABCInput *)input completion:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion')); + }); + + test('async output(void) HostApi header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'void', + returnType: 'Output', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '(void)doSomething:(void(^)(ABCOutput *_Nullable, FlutterError *_Nullable))completion')); + }); + + test('async void(void) HostApi header', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'void', + returnType: 'void', + isAsynchronous: true) + ]) + ], classes: []); + final StringBuffer sink = StringBuffer(); + generateObjcHeader(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '(void)doSomething:(void(^)(FlutterError *_Nullable))completion')); + }); + + test('async output(input) HostApi source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'Input', + returnType: 'Output', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '[api doSomething:input completion:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {')); + }); + + test('async void(input) HostApi source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'Input', + returnType: 'void', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Input', + fields: [Field(name: 'input', dataType: 'String')]), + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '[api doSomething:input completion:^(FlutterError *_Nullable error) {')); + }); + + test('async void(void) HostApi source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'void', + returnType: 'void', + isAsynchronous: true) + ]) + ], classes: []); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, contains('[api doSomething:^(FlutterError *_Nullable error) {')); + }); + + test('async output(void) HostApi source', () { + final Root root = Root(apis: [ + Api(name: 'Api', location: ApiLocation.host, methods: [ + Method( + name: 'doSomething', + argType: 'void', + returnType: 'Output', + isAsynchronous: true) + ]) + ], classes: [ + Class( + name: 'Output', + fields: [Field(name: 'output', dataType: 'String')]), + ]); + final StringBuffer sink = StringBuffer(); + generateObjcSource(ObjcOptions(header: 'foo.h', prefix: 'ABC'), root, sink); + final String code = sink.toString(); + expect( + code, + contains( + '[api doSomething:^(ABCOutput *_Nullable output, FlutterError *_Nullable error) {')); + }); }