diff --git a/packages/pigeon/.gitignore b/packages/pigeon/.gitignore
new file mode 100644
index 000000000000..add3fc06234a
--- /dev/null
+++ b/packages/pigeon/.gitignore
@@ -0,0 +1,2 @@
+build/
+e2e_tests/test_objc/ios/Flutter/
diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md
new file mode 100644
index 000000000000..e0f8d17e6367
--- /dev/null
+++ b/packages/pigeon/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0-experimental.0
+
+* Initial release.
diff --git a/packages/pigeon/LICENSE b/packages/pigeon/LICENSE
new file mode 100644
index 000000000000..bc67b8f95568
--- /dev/null
+++ b/packages/pigeon/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2019 The Chromium Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/pigeon/README.md b/packages/pigeon/README.md
new file mode 100644
index 000000000000..916cbd9ac186
--- /dev/null
+++ b/packages/pigeon/README.md
@@ -0,0 +1,126 @@
+# Pigeon
+
+
+
+Pigeon is a code generator tool to make communication between Flutter and the
+host platform type-safe and easier.
+
+## Supported Platforms
+
+Currently Pigeon only supports generating Objective-C code for usage on iOS and calling host functions from Flutter.
+
+## Runtime Requirements
+
+Pigeon generates all the code that is needed to communicate between Flutter and the host platform, there is no extra runtime requirement. A plugin author doesn't need to worry about conflicting versions of Pigeon.
+
+## Usage
+
+### Steps
+
+1) Add Pigeon as a dev_dependency.
+1) Make a ".dart" file outside of your "lib" directory for defining the communication interface.
+1) Run pigeon on your ".dart" file to generate the required Dart and Objective-C code.
+1) Add the generated code to your `ios/Runner.xcworkspace` XCode project for compilation.
+1) Implement the generated iOS protocol for handling the calls on iOS, set it up
+ as the handler for the messages.
+1) Call the generated Dart methods.
+
+### Rules for defining your communication interface
+
+1) The file should contain no methods or function definitions.
+1) Datatypes are defined as classes with fields of the supported datatypes (see
+ the supported Datatypes section).
+1) Api's should be defined as an `abstract class` with either `HostApi()` or
+ `FlutterApi()` as metadata. The former being for procedures that are defined
+ on the host platform and the latter for procedures that are defined in Dart.
+1) Method declarations on the Api classes should have one argument and a return
+ value whose types are defined in the file.
+
+### Example
+
+#### message.dart
+
+```dart
+import 'package:pigeon/pigeon_lib.dart';
+
+class SearchRequest {
+ String query;
+}
+
+class SearchReply {
+ String result;
+}
+
+@HostApi()
+abstract class Api {
+ SearchReply search(SearchRequest request);
+}
+```
+
+#### invocation
+
+```sh
+pub run pigeon \
+ --input pigeons/message.dart \
+ --dart_out lib/pigeon.dart \
+ --objc_header_out ios/Runner/pigeon.h \
+ --objc_source_out ios/Runner/pigeon.m
+```
+
+#### AppDelegate.m
+
+```objc
+#import "AppDelegate.h"
+#import
+#import "pigeon.h"
+
+@interface MyApi : NSObject
+@end
+
+@implementation MyApi
+-(SearchReply*)search:(SearchRequest*)request {
+ SearchReply *reply = [[SearchReply alloc] init];
+ reply.result =
+ [NSString stringWithFormat:@"Hi %@!", request.query];
+ return reply;
+}
+@end
+
+- (BOOL)application:(UIApplication *)application
+didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ MyApi *api = [[MyApi alloc] init];
+ ApiSetup(getFlutterEngine().binaryMessenger, api);
+ return YES;
+}
+```
+
+#### test.dart
+
+```dart
+import 'pigeon.dart';
+
+void main() {
+ testWidgets("test pigeon", (WidgetTester tester) async {
+ SearchRequest request = SearchRequest()..query = "Aaron";
+ Api api = Api();
+ SearchReply reply = await api.search(request);
+ expect(reply.result, equals("Hi Aaron!"));
+ });
+}
+
+```
+
+## Supported Datatypes
+
+Pigeon uses the `StandardMessageCodec` so it supports any data-type platform
+channels supports
+[[documentation](https://flutter.dev/docs/development/platform-integration/platform-channels#codec)]. Nested data-types are supported, too.
+
+Note: Generics for List and Map aren't supported yet.
+
+## Feedback
+
+File an issue in [flutter/flutter](https://github.com/flutter/flutter) with the
+word 'pigeon' clearly in the title and cc **@gaaclarke**.
diff --git a/packages/pigeon/bin/pigeon.dart b/packages/pigeon/bin/pigeon.dart
new file mode 100644
index 000000000000..e203bf26ae6b
--- /dev/null
+++ b/packages/pigeon/bin/pigeon.dart
@@ -0,0 +1,32 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'package:pigeon/pigeon_lib.dart';
+
+Future main(List args) async {
+ final PigeonOptions opts = Pigeon.parseArgs(args);
+ assert(opts.input != null);
+ final String importLine =
+ (opts.input != null) ? 'import \'${opts.input}\';\n' : '';
+ final String code = """$importLine
+import 'dart:io';
+import 'package:pigeon/pigeon_lib.dart';
+
+void main(List args) async {
+ exit(await Pigeon.run(args));
+}
+""";
+ // TODO(aaclarke): Start using a system temp file.
+ const String tempFilename = '_pigeon_temp_.dart';
+ final File tempFile = await File(tempFilename).writeAsString(code);
+ final Process process =
+ await Process.start('dart', [tempFilename] + args);
+ process.stdout.transform(utf8.decoder).listen((String data) => print(data));
+ process.stderr.transform(utf8.decoder).listen((String data) => print(data));
+ final int exitCode = await process.exitCode;
+ tempFile.deleteSync();
+ exit(exitCode);
+}
diff --git a/packages/pigeon/lib/ast.dart b/packages/pigeon/lib/ast.dart
new file mode 100644
index 000000000000..6c0e42d70ce9
--- /dev/null
+++ b/packages/pigeon/lib/ast.dart
@@ -0,0 +1,81 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Enum that represents where an [Api] is located, on the host or Flutter.
+enum ApiLocation {
+ /// The API is for calling functions defined on the host.
+ host,
+
+ /// The API is for calling functions defined in Flutter.
+ flutter,
+}
+
+/// Superclass for all AST nodes.
+class Node {}
+
+/// Represents a method on an [Api].
+class Method extends Node {
+ /// Parametric constructor for [Method].
+ Method({this.name, this.returnType, this.argType});
+
+ /// The name of the method.
+ String name;
+
+ /// The data-type of the return value.
+ String returnType;
+
+ /// The data-type of the argument.
+ String argType;
+}
+
+/// Represents a collection of [Method]s that are hosted ona given [location].
+class Api extends Node {
+ /// Parametric constructor for [Api].
+ Api({this.name, this.location, this.methods});
+
+ /// The name of the API.
+ String name;
+
+ /// Where the API's implementation is located, host or Flutter.
+ ApiLocation location;
+
+ /// List of methods inside the API.
+ List methods;
+}
+
+/// Represents a field on a [Class].
+class Field extends Node {
+ /// Parametric constructor for [Field].
+ Field({this.name, this.dataType});
+
+ /// The name of the field.
+ String name;
+
+ /// The data-type of the field (ex 'String' or 'int').
+ String dataType;
+}
+
+/// Represents a class with [Field]s.
+class Class extends Node {
+ /// Parametric constructor for [Class].
+ Class({this.name, this.fields});
+
+ /// The name of the class.
+ String name;
+
+ /// All the fields contained in the class.
+ List fields;
+}
+
+/// Top-level node for the AST.
+class Root extends Node {
+ /// Parametric constructor for [Root].
+ Root({this.classes, this.apis});
+
+ /// All the classes contained in the AST.
+ List classes;
+
+ /// All the API's contained in the AST.
+ List apis;
+}
diff --git a/packages/pigeon/lib/dart_generator.dart b/packages/pigeon/lib/dart_generator.dart
new file mode 100644
index 000000000000..c249a883ab9c
--- /dev/null
+++ b/packages/pigeon/lib/dart_generator.dart
@@ -0,0 +1,79 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Generates Dart source code for the given AST represented by [root],
+/// outputting the code to [sink].
+void generateDart(Root root, StringSink sink) {
+ final List customClassNames =
+ root.classes.map((Class x) => x.name).toList();
+ final Indent indent = Indent(sink);
+ indent.writeln('// Autogenerated from Dartle, do not edit directly.');
+ indent.writeln('import \'package:flutter/services.dart\';');
+ indent.writeln('');
+
+ for (Class klass in root.classes) {
+ sink.write('class ${klass.name} ');
+ indent.scoped('{', '}', () {
+ for (Field field in klass.fields) {
+ indent.writeln('${field.dataType} ${field.name};');
+ }
+ indent.write('Map _toMap() ');
+ indent.scoped('{', '}', () {
+ indent.writeln('Map dartleMap = Map();');
+ for (Field field in klass.fields) {
+ indent.write('dartleMap["${field.name}"] = ');
+ if (customClassNames.contains(field.dataType)) {
+ indent.addln('${field.name}._toMap();');
+ } else {
+ indent.addln('${field.name};');
+ }
+ }
+ indent.writeln('return dartleMap;');
+ });
+ indent.write('static ${klass.name} _fromMap(Map dartleMap) ');
+ indent.scoped('{', '}', () {
+ indent.writeln('var result = ${klass.name}();');
+ for (Field field in klass.fields) {
+ indent.write('result.${field.name} = ');
+ if (customClassNames.contains(field.dataType)) {
+ indent.addln(
+ '${field.dataType}._fromMap(dartleMap["${field.name}"]);');
+ } else {
+ indent.addln('dartleMap["${field.name}"];');
+ }
+ }
+ indent.writeln('return result;');
+ });
+ });
+ indent.writeln('');
+ }
+ for (Api api in root.apis) {
+ if (api.location == ApiLocation.host) {
+ indent.write('class ${api.name} ');
+ indent.scoped('{', '}', () {
+ for (Method func in api.methods) {
+ indent.write(
+ 'Future<${func.returnType}> ${func.name}(${func.argType} arg) async ');
+ indent.scoped('{', '}', () {
+ indent.writeln('Map requestMap = arg._toMap();');
+ final String channelName = makeChannelName(api, func);
+ indent.writeln('BasicMessageChannel channel =');
+ indent.inc();
+ indent.inc();
+ indent.writeln(
+ 'BasicMessageChannel(\'$channelName\', StandardMessageCodec());');
+ indent.dec();
+ indent.dec();
+ indent.writeln('Map replyMap = await channel.send(requestMap);');
+ indent.writeln('return ${func.returnType}._fromMap(replyMap);');
+ });
+ }
+ });
+ indent.writeln('');
+ }
+ }
+}
diff --git a/packages/pigeon/lib/generator_tools.dart b/packages/pigeon/lib/generator_tools.dart
new file mode 100644
index 000000000000..db662aa2f69a
--- /dev/null
+++ b/packages/pigeon/lib/generator_tools.dart
@@ -0,0 +1,84 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+import 'ast.dart';
+
+/// Read all the content from [stdin] to a String.
+String readStdin() {
+ final List bytes = [];
+ int byte = stdin.readByteSync();
+ while (byte >= 0) {
+ bytes.add(byte);
+ byte = stdin.readByteSync();
+ }
+ return utf8.decode(bytes);
+}
+
+/// A helper class for managing indentation, wrapping a [StringSink].
+class Indent {
+ /// Constructor which takes a [StringSink] [Ident] will wrap.
+ Indent(this._sink);
+
+ int _count = 0;
+ final StringSink _sink;
+
+ /// String used for newlines (ex "\n").
+ final String newline = '\n';
+
+ /// Increase the indentation level.
+ void inc() {
+ _count += 1;
+ }
+
+ /// Decrement the indentation level.
+ void dec() {
+ _count -= 1;
+ }
+
+ /// Returns the String represneting the current indentation.
+ String str() {
+ String result = '';
+ for (int i = 0; i < _count; i++) {
+ result += ' ';
+ }
+ return result;
+ }
+
+ /// Scoped increase of the ident level. For the execution of [func] the
+ /// indentation will be incremented.
+ void scoped(String begin, String end, Function func) {
+ _sink.write(begin + newline);
+ inc();
+ func();
+ dec();
+ _sink.write(str() + end + newline);
+ }
+
+ /// Add [str] with indentation and a newline.
+ void writeln(String str) {
+ _sink.write(this.str() + str + newline);
+ }
+
+ /// Add [str] with indentation.
+ void write(String str) {
+ _sink.write(this.str() + str);
+ }
+
+ /// Add [str] with a newline.
+ void addln(String str) {
+ _sink.write(str + newline);
+ }
+
+ /// Just adds [str].
+ void add(String str) {
+ _sink.write(str);
+ }
+}
+
+/// Create the generated channel name for a [func] on a [api].
+String makeChannelName(Api api, Method func) {
+ return 'dev.flutter.dartle.${api.name}.${func.name}';
+}
diff --git a/packages/pigeon/lib/objc_generator.dart b/packages/pigeon/lib/objc_generator.dart
new file mode 100644
index 000000000000..5b59d412e0c7
--- /dev/null
+++ b/packages/pigeon/lib/objc_generator.dart
@@ -0,0 +1,217 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'ast.dart';
+import 'generator_tools.dart';
+
+/// Options that control how Objective-C code will be generated.
+class ObjcOptions {
+ /// Parametric constructor for ObjcOptions.
+ ObjcOptions({this.header, this.prefix});
+
+ /// The path to the header that will get placed in the source filed (example:
+ /// "foo.h").
+ String header;
+
+ /// Prefix that will be appended before all generated classes and protocols.
+ String prefix;
+}
+
+String _className(String prefix, String className) {
+ if (prefix != null) {
+ return '$prefix$className';
+ } else {
+ return className;
+ }
+}
+
+const Map _objcTypeForDartTypeMap = {
+ 'bool': 'NSNumber *',
+ 'int': 'NSNumber *',
+ 'String': 'NSString *',
+ 'double': 'NSNumber *',
+ 'Uint8List': 'FlutterStandardTypedData *',
+ 'Int32List': 'FlutterStandardTypedData *',
+ 'Int64List': 'FlutterStandardTypedData *',
+ 'Float64List': 'FlutterStandardTypedData *',
+};
+
+const Map _propertyTypeForDartTypeMap = {
+ 'String': 'copy',
+ 'bool': 'strong',
+ 'int': 'strong',
+ 'double': 'strong',
+ 'Uint8List': 'strong',
+ 'Int32List': 'strong',
+ 'Int64List': 'strong',
+ 'Float64List': 'strong',
+};
+
+String _objcTypeForDartType(String type) {
+ return _objcTypeForDartTypeMap[type];
+}
+
+String _propertyTypeForDartType(String type) {
+ final String result = _propertyTypeForDartTypeMap[type];
+ if (result == null) {
+ return 'assign';
+ } else {
+ return result;
+ }
+}
+
+/// Generates the ".h" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcHeader(ObjcOptions options, Root root, StringSink sink) {
+ final Indent indent = Indent(sink);
+ indent.writeln('// Autogenerated from Dartle.');
+ indent.writeln('#import ');
+ indent.writeln('@protocol FlutterBinaryMessenger;');
+ indent.writeln('@class FlutterStandardTypedData;');
+ indent.writeln('');
+
+ for (Class klass in root.classes) {
+ indent.writeln('@class ${_className(options.prefix, klass.name)};');
+ }
+
+ indent.writeln('');
+
+ for (Class klass in root.classes) {
+ indent.writeln(
+ '@interface ${_className(options.prefix, klass.name)} : NSObject ');
+ for (Field field in klass.fields) {
+ String propertyType = _propertyTypeForDartType(field.dataType);
+ String objcType = _objcTypeForDartType(field.dataType);
+ if (objcType == null &&
+ root.classes.map((Class x) => x.name).contains(field.dataType)) {
+ propertyType = 'strong';
+ objcType = '${_className(options.prefix, field.dataType)} *';
+ }
+ indent.writeln(
+ '@property(nonatomic, $propertyType) $objcType ${field.name};');
+ }
+ indent.writeln('@end');
+ indent.writeln('');
+ }
+
+ for (Api api in root.apis) {
+ if (api.location == ApiLocation.host) {
+ final String apiName = _className(options.prefix, api.name);
+ indent.writeln('@protocol $apiName');
+ for (Method func in api.methods) {
+ final String returnType = _className(options.prefix, func.returnType);
+ final String argType = _className(options.prefix, func.argType);
+ indent.writeln('-($returnType *)${func.name}:($argType*)input;');
+ }
+ indent.writeln('@end');
+ indent.writeln('');
+ indent.writeln(
+ 'extern void ${apiName}Setup(id binaryMessenger, id<$apiName> api);');
+ indent.writeln('');
+ }
+ }
+}
+
+String _dictGetter(
+ List classnames, String dict, Field field, String prefix) {
+ if (classnames.contains(field.dataType)) {
+ String className = field.dataType;
+ if (prefix != null) {
+ className = '$prefix$className';
+ }
+ return '[$className fromMap:$dict[@"${field.name}"]]';
+ } else {
+ return '$dict[@"${field.name}"]';
+ }
+}
+
+String _dictValue(List classnames, Field field) {
+ if (classnames.contains(field.dataType)) {
+ return '[self.${field.name} toMap]';
+ } else {
+ return 'self.${field.name}';
+ }
+}
+
+/// Generates the ".m" file for the AST represented by [root] to [sink] with the
+/// provided [options].
+void generateObjcSource(ObjcOptions options, Root root, StringSink sink) {
+ final Indent indent = Indent(sink);
+ final List classnames =
+ root.classes.map((Class x) => x.name).toList();
+
+ indent.writeln('// Autogenerated from Dartle.');
+ indent.writeln('#import "${options.header}"');
+ indent.writeln('#import ');
+ indent.writeln('');
+
+ for (Class klass in root.classes) {
+ final String className = _className(options.prefix, klass.name);
+ indent.writeln('@interface $className ()');
+ indent.writeln('+($className*)fromMap:(NSDictionary*)dict;');
+ indent.writeln('-(NSDictionary*)toMap;');
+ indent.writeln('@end');
+ }
+
+ indent.writeln('');
+
+ for (Class klass in root.classes) {
+ final String className = _className(options.prefix, klass.name);
+ indent.writeln('@implementation $className');
+ indent.write('+($className*)fromMap:(NSDictionary*)dict ');
+ indent.scoped('{', '}', () {
+ indent.writeln('$className* result = [[$className alloc] init];');
+ for (Field field in klass.fields) {
+ indent.writeln(
+ 'result.${field.name} = ${_dictGetter(classnames, 'dict', field, options.prefix)};');
+ }
+ indent.writeln('return result;');
+ });
+ indent.write('-(NSDictionary*)toMap ');
+ indent.scoped('{', '}', () {
+ indent.write('return [NSDictionary dictionaryWithObjectsAndKeys:');
+ for (Field field in klass.fields) {
+ indent.add(_dictValue(classnames, field) + ', @"${field.name}", ');
+ }
+ indent.addln('nil];');
+ });
+ indent.writeln('@end');
+ indent.writeln('');
+ }
+
+ for (Api api in root.apis) {
+ if (api.location == ApiLocation.host) {
+ final String apiName = _className(options.prefix, api.name);
+ indent.write(
+ 'void ${apiName}Setup(id binaryMessenger, id<$apiName> api) ');
+ indent.scoped('{', '}', () {
+ for (Method func in api.methods) {
+ indent.write('');
+ indent.scoped('{', '}', () {
+ indent.writeln('FlutterBasicMessageChannel *channel =');
+ indent.inc();
+ indent.writeln('[FlutterBasicMessageChannel');
+ indent.inc();
+ indent.writeln(
+ 'messageChannelWithName:@"${makeChannelName(api, func)}"');
+ indent.writeln('binaryMessenger:binaryMessenger];');
+ indent.dec();
+ indent.dec();
+
+ indent.write(
+ '[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) ');
+ indent.scoped('{', '}];', () {
+ final String argType = _className(options.prefix, func.argType);
+ final String returnType =
+ _className(options.prefix, func.returnType);
+ indent.writeln('$argType *input = [$argType fromMap:message];');
+ indent.writeln('$returnType *output = [api ${func.name}:input];');
+ indent.writeln('callback([output toMap]);');
+ });
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/packages/pigeon/lib/pigeon_lib.dart b/packages/pigeon/lib/pigeon_lib.dart
new file mode 100644
index 000000000000..2b58649526f8
--- /dev/null
+++ b/packages/pigeon/lib/pigeon_lib.dart
@@ -0,0 +1,327 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+import 'dart:mirrors';
+
+import 'package:args/args.dart';
+import 'package:path/path.dart';
+
+import 'ast.dart';
+import 'dart_generator.dart';
+import 'objc_generator.dart';
+
+const List _validTypes = [
+ 'String',
+ 'int',
+ 'double',
+ 'Uint8List',
+ 'Int32List',
+ 'Int64List',
+ 'Float64List',
+ 'List',
+ 'Map',
+];
+
+/// Metadata to mark an API which will be implemented on the host platform.
+class HostApi {
+ /// Parametric constructor for [HostApi].
+ const HostApi();
+}
+
+/// Metadata to mark an API which will be implemented in Flutter.
+class FlutterApi {
+ /// Parametric constructor for [FlutterApi].
+ const FlutterApi();
+}
+
+/// Represents an error as a result of parsing and generating code.
+class Error {
+ /// Parametric constructor for Error.
+ Error({this.message, this.filename, this.lineNumber});
+
+ /// A description of the error.
+ String message;
+
+ /// What file caused the [Error].
+ String filename;
+
+ /// What line the error happened on.
+ int lineNumber;
+}
+
+bool _isApi(ClassMirror classMirror) {
+ return classMirror.isAbstract && _isHostApi(classMirror);
+}
+
+bool _isHostApi(ClassMirror apiMirror) {
+ for (InstanceMirror instance in apiMirror.metadata) {
+ if (instance.reflectee is HostApi) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Options used when running the code generator.
+class PigeonOptions {
+ /// Path to the file which will be processed.
+ String input;
+
+ /// Path to the dart file that will be generated.
+ String dartOut;
+
+ /// Path to the ".h" Objective-C file will be generated.
+ String objcHeaderOut;
+
+ /// Path to the ".m" Objective-C file will be generated.
+ String objcSourceOut;
+
+ /// Options that control how Objective-C will be generated.
+ ObjcOptions objcOptions = ObjcOptions();
+}
+
+/// A collection of an AST represented as a [Root] and [Error]'s.
+class ParseResults {
+ /// Parametric constructor for [ParseResults].
+ ParseResults({this.root, this.errors});
+
+ /// The resulting AST.
+ final Root root;
+
+ /// Errors generated while parsing input.
+ final List errors;
+}
+
+/// Tool for generating code to facilitate platform channels usage.
+class Pigeon {
+ /// Create and setup a [Pigeon] instance.
+ static Pigeon setup() {
+ return Pigeon();
+ }
+
+ Class _parseClassMirror(ClassMirror klassMirror) {
+ final List fields = [];
+ for (DeclarationMirror declaration in klassMirror.declarations.values) {
+ if (declaration is VariableMirror) {
+ fields.add(Field()
+ ..name = MirrorSystem.getName(declaration.simpleName)
+ ..dataType = MirrorSystem.getName(declaration.type.simpleName));
+ }
+ }
+ final Class klass = Class()
+ ..name = MirrorSystem.getName(klassMirror.simpleName)
+ ..fields = fields;
+ return klass;
+ }
+
+ /// Use reflection to parse the [types] provided.
+ ParseResults parse(List types) {
+ final Root root = Root();
+ final Set classes = {};
+ final List apis = [];
+
+ for (Type type in types) {
+ final ClassMirror classMirror = reflectClass(type);
+ if (_isApi(classMirror)) {
+ apis.add(classMirror);
+ } else {
+ classes.add(classMirror);
+ }
+ }
+
+ for (ClassMirror apiMirror in apis) {
+ for (DeclarationMirror declaration in apiMirror.declarations.values) {
+ if (declaration is MethodMirror && !declaration.isConstructor) {
+ classes.add(declaration.returnType);
+ classes.add(declaration.parameters[0].type);
+ }
+ }
+ }
+
+ root.classes = classes.map(_parseClassMirror).toList();
+
+ root.apis = [];
+ for (ClassMirror apiMirror in apis) {
+ if (_isHostApi(apiMirror)) {
+ final List methods = [];
+ for (DeclarationMirror declaration in apiMirror.declarations.values) {
+ if (declaration is MethodMirror && !declaration.isConstructor) {
+ methods.add(Method()
+ ..name = MirrorSystem.getName(declaration.simpleName)
+ ..argType = MirrorSystem.getName(
+ declaration.parameters[0].type.simpleName)
+ ..returnType =
+ MirrorSystem.getName(declaration.returnType.simpleName));
+ }
+ }
+ root.apis.add(Api()
+ ..name = MirrorSystem.getName(apiMirror.simpleName)
+ ..location = ApiLocation.host
+ ..methods = methods);
+ }
+ }
+
+ final List validateErrors = _validateAst(root);
+ return ParseResults(root: root, errors: validateErrors);
+ }
+
+ /// String that describes how the tool is used.
+ static String get usage {
+ return '''
+
+Pigeon is a tool for generating type-safe communication code between Flutter
+and the host platform.
+
+usage: pigeon --input --dart_out [option]*
+
+options:
+''' +
+ _argParser.usage;
+ }
+
+ static final ArgParser _argParser = ArgParser()
+ ..addOption('input', help: 'REQUIRED: Path to pigeon file.')
+ ..addOption('dart_out',
+ help: 'REQUIRED: Path to generated dart source file (.dart).')
+ ..addOption('objc_source_out',
+ help: 'Path to generated Objective-C source file (.m).')
+ ..addOption('objc_header_out',
+ help: 'Path to generated Objective-C header file (.h).')
+ ..addOption('objc_prefix',
+ help: 'Prefix for generated Objective-C classes and protocols.');
+
+ /// Convert command-line arugments to [PigeonOptions].
+ static PigeonOptions parseArgs(List args) {
+ final ArgResults results = _argParser.parse(args);
+
+ final PigeonOptions opts = PigeonOptions();
+ opts.input = results['input'];
+ opts.dartOut = results['dart_out'];
+ opts.objcHeaderOut = results['objc_header_out'];
+ opts.objcSourceOut = results['objc_source_out'];
+ opts.objcOptions.prefix = results['objc_prefix'];
+ return opts;
+ }
+
+ static Future _runGenerator(
+ String output, void Function(IOSink sink) func) async {
+ IOSink sink;
+ File file;
+ if (output == 'stdout') {
+ sink = stdout;
+ } else {
+ file = File(output);
+ sink = file.openWrite();
+ }
+ func(sink);
+ await sink.flush();
+ }
+
+ List _validateAst(Root root) {
+ final List result = [];
+ final List customClasses =
+ root.classes.map((Class x) => x.name).toList();
+ for (Class klass in root.classes) {
+ for (Field field in klass.fields) {
+ if (!(_validTypes.contains(field.dataType) ||
+ customClasses.contains(field.dataType))) {
+ result.add(Error(
+ message:
+ 'Unsupported datatype:"${field.dataType}" in class "${klass.name}".'));
+ }
+ }
+ }
+ return result;
+ }
+
+ /// Crawls through the reflection system looking for a setupPigeon method and
+ /// executing it.
+ static void _executeSetupPigeon(PigeonOptions options) {
+ for (LibraryMirror library in currentMirrorSystem().libraries.values) {
+ for (DeclarationMirror declaration in library.declarations.values) {
+ if (declaration is MethodMirror &&
+ MirrorSystem.getName(declaration.simpleName) == 'setupPigeon') {
+ if (declaration.parameters.length == 1 &&
+ declaration.parameters[0].type == reflectClass(PigeonOptions)) {
+ library.invoke(declaration.simpleName, [options]);
+ } else {
+ print('warning: invalid \'setupPigeon\' method defined.');
+ }
+ }
+ }
+ }
+ }
+
+ /// The 'main' entrypoint used by the command-line tool. [args] are the
+ /// command-line arguments.
+ static Future run(List args) async {
+ final Pigeon pigeon = Pigeon.setup();
+ final PigeonOptions options = Pigeon.parseArgs(args);
+
+ _executeSetupPigeon(options);
+
+ if (options.input == null || options.dartOut == null) {
+ print(usage);
+ return 0;
+ }
+
+ final List errors = [];
+ final List apis = [];
+ options.objcOptions.header = basename(options.objcHeaderOut);
+
+ for (LibraryMirror library in currentMirrorSystem().libraries.values) {
+ for (DeclarationMirror declaration in library.declarations.values) {
+ if (declaration is ClassMirror && _isApi(declaration)) {
+ apis.add(declaration.reflectedType);
+ }
+ }
+ }
+
+ if (apis.isNotEmpty) {
+ final ParseResults parseResults = pigeon.parse(apis);
+ for (Error err in parseResults.errors) {
+ errors.add(Error(message: err.message, filename: options.input));
+ }
+ if (options.dartOut != null) {
+ await _runGenerator(options.dartOut,
+ (StringSink sink) => generateDart(parseResults.root, sink));
+ }
+ if (options.objcHeaderOut != null) {
+ await _runGenerator(
+ options.objcHeaderOut,
+ (StringSink sink) => generateObjcHeader(
+ options.objcOptions, parseResults.root, sink));
+ }
+ if (options.objcSourceOut != null) {
+ await _runGenerator(
+ options.objcSourceOut,
+ (StringSink sink) => generateObjcSource(
+ options.objcOptions, parseResults.root, sink));
+ }
+ } else {
+ errors.add(Error(message: 'No pigeon classes found, nothing generated.'));
+ }
+
+ printErrors(errors);
+
+ return errors.isNotEmpty ? 1 : 0;
+ }
+
+ /// Print a list of errors to stderr.
+ static void printErrors(List errors) {
+ for (Error err in errors) {
+ if (err.filename != null) {
+ if (err.lineNumber != null) {
+ stderr.writeln(
+ 'Error: ${err.filename}:${err.lineNumber}: ${err.message}');
+ } else {
+ stderr.writeln('Error: ${err.filename}: ${err.message}');
+ }
+ } else {
+ stderr.writeln('Error: ${err.message}');
+ }
+ }
+ }
+}
diff --git a/packages/pigeon/pigeons/message.dart b/packages/pigeon/pigeons/message.dart
new file mode 100644
index 000000000000..c99fc8f9684e
--- /dev/null
+++ b/packages/pigeon/pigeons/message.dart
@@ -0,0 +1,31 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:pigeon/pigeon_lib.dart';
+
+class SearchRequest {
+ String query;
+}
+
+class SearchReply {
+ String result;
+}
+
+@HostApi()
+abstract class Api {
+ SearchReply search(SearchRequest request);
+}
+
+class Nested {
+ SearchRequest request;
+}
+
+@HostApi()
+abstract class NestedApi {
+ SearchReply search(Nested nested);
+}
+
+void setupPigeon(PigeonOptions options) {
+ options.objcOptions.prefix = 'AC';
+}
diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml
new file mode 100644
index 000000000000..ae831aaeaf0b
--- /dev/null
+++ b/packages/pigeon/pubspec.yaml
@@ -0,0 +1,11 @@
+name: pigeon
+version: 0.1.0-experimental.0
+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:
+ path: ^1.6.4
+ args: ^1.5.2
+dev_dependencies:
+ test: ^1.11.1
+environment:
+ sdk: '>=2.2.0 <3.0.0'
\ No newline at end of file
diff --git a/packages/pigeon/run_tests.sh b/packages/pigeon/run_tests.sh
new file mode 100755
index 000000000000..cc0f99bf9214
--- /dev/null
+++ b/packages/pigeon/run_tests.sh
@@ -0,0 +1,40 @@
+# exit when any command fails
+set -e
+
+pub run test test/
+
+# cd build
+# make
+# ./dartle --input ../dartles/simple.dartle --dart_out dartle.dart --objc_header_out dartle.h --objc_source_out dartle.m
+# cd ..
+# cp build/dartle.h tests/test_objc/ios/Runner/
+# cp build/dartle.m tests/test_objc/ios/Runner/
+# cp build/dartle.dart tests/test_objc/lib/
+
+# e2e tests are disabled while I work to fix iOS e2e.
+# DARTLE_H="e2e_tests/test_objc/ios/Runner/dartle.h"
+# DARTLE_M="e2e_tests/test_objc/ios/Runner/dartle.m"
+# DARTLE_DART="e2e_tests/test_objc/lib/dartle.dart"
+# pub run pigeon \
+# --input pigeons/message.dart \
+# --dart_out $DARTLE_DART \
+# --objc_header_out $DARTLE_H \
+# --objc_source_out $DARTLE_M
+# dartfmt -w $DARTLE_DART
+# cd e2e_tests/test_objc
+
+#############################################
+# Uncomment to just launch the app.
+#############################################
+# open -a Simulator
+# flutter run
+# exit
+
+# flutter build ios -t test/e2e_test.dart --simulator
+# cd ios
+# xcodebuild \
+# -workspace Runner.xcworkspace \
+# -scheme RunnerTests \
+# -sdk iphonesimulator \
+# -destination 'platform=iOS Simulator,name=iPhone 8' \
+# test | xcpretty
diff --git a/packages/pigeon/test/dart_generator_test.dart b/packages/pigeon/test/dart_generator_test.dart
new file mode 100644
index 000000000000..332e40165bc8
--- /dev/null
+++ b/packages/pigeon/test/dart_generator_test.dart
@@ -0,0 +1,64 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/dart_generator.dart';
+import 'package:pigeon/ast.dart';
+
+void main() {
+ test('gen one class', () {
+ final Class klass = Class()
+ ..name = 'Foobar'
+ ..fields = [
+ Field()
+ ..name = 'field1'
+ ..dataType = 'dataType1'
+ ];
+ final Root root = Root()
+ ..apis = []
+ ..classes = [klass];
+ final StringBuffer sink = StringBuffer();
+ generateDart(root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Foobar'));
+ expect(code, contains(' dataType1 field1;'));
+ });
+
+ test('gen one host api', () {
+ final Root root = Root(apis: [
+ Api(name: 'Api', location: ApiLocation.host, methods: [
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Output',
+ fields: [Field(name: 'output', dataType: 'String')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(root, sink);
+ final String code = sink.toString();
+ expect(code, contains('class Api'));
+ expect(code, matches('Output.*doSomething.*Input'));
+ });
+
+ test('nested class', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: [Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateDart(root, sink);
+ final String code = sink.toString();
+ expect(code, contains('dartleMap["nested"] = nested._toMap()'));
+ expect(
+ code, contains('result.nested = Input._fromMap(dartleMap["nested"]);'));
+ });
+}
diff --git a/packages/pigeon/test/objc_generator_test.dart b/packages/pigeon/test/objc_generator_test.dart
new file mode 100644
index 000000000000..cac11f822283
--- /dev/null
+++ b/packages/pigeon/test/objc_generator_test.dart
@@ -0,0 +1,225 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/objc_generator.dart';
+import 'package:pigeon/ast.dart';
+
+void main() {
+ test('gen one class header', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Foobar',
+ fields: [Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, matches('@property.*NSString.*field1'));
+ });
+
+ test('gen one class source', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Foobar',
+ fields: [Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('#import \"foo.h\"'));
+ expect(code, contains('@implementation Foobar'));
+ });
+
+ test('gen one api header', () {
+ final Root root = Root(apis: [
+ Api(name: 'Api', location: ApiLocation.host, methods: [
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], 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(), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Input'));
+ expect(code, contains('@interface Output'));
+ expect(code, contains('@protocol Api'));
+ expect(code, matches('Output.*doSomething.*Input'));
+ expect(code, contains('ApiSetup('));
+ });
+
+ test('gen one api source', () {
+ final Root root = Root(apis: [
+ Api(name: 'Api', location: ApiLocation.host, methods: [
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Output')
+ ])
+ ], 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'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('#import "foo.h"'));
+ expect(code, contains('@implementation Input'));
+ expect(code, contains('@implementation Output'));
+ expect(code, contains('ApiSetup('));
+ });
+
+ test('all the simple datatypes header', () {
+ final Root root = Root(apis: [], classes: [
+ Class(name: 'Foobar', fields: [
+ Field(name: 'aBool', dataType: 'bool'),
+ Field(name: 'aInt', dataType: 'int'),
+ Field(name: 'aDouble', dataType: 'double'),
+ Field(name: 'aString', dataType: 'String'),
+ Field(name: 'aUint8List', dataType: 'Uint8List'),
+ Field(name: 'aInt32List', dataType: 'Int32List'),
+ Field(name: 'aInt64List', dataType: 'Int64List'),
+ Field(name: 'aFloat64List', dataType: 'Float64List'),
+ ]),
+ ]);
+
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface Foobar'));
+ expect(code, contains('@class FlutterStandardTypedData;'));
+ expect(code, matches('@property.*strong.*NSNumber.*aBool'));
+ expect(code, matches('@property.*strong.*NSNumber.*aInt'));
+ expect(code, matches('@property.*strong.*NSNumber.*aDouble'));
+ expect(code, matches('@property.*copy.*NSString.*aString'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*aUint8List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*aInt32List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*Int64List'));
+ expect(code,
+ matches('@property.*strong.*FlutterStandardTypedData.*Float64List'));
+ });
+
+ test('bool source', () {
+ final Root root = Root(apis: [], classes: [
+ Class(name: 'Foobar', fields: [
+ Field(name: 'aBool', dataType: 'bool'),
+ ]),
+ ]);
+
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@implementation Foobar'));
+ expect(code, contains('result.aBool = dict[@\"aBool\"];'));
+ });
+
+ test('nested class header', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: [Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@property(nonatomic, strong) Input * nested;'));
+ });
+
+ test('nested class source', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: [Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(header: 'foo.h'), root, sink);
+ final String code = sink.toString();
+ expect(
+ code, contains('result.nested = [Input fromMap:dict[@\"nested\"]];'));
+ expect(code, contains('[self.nested toMap], @\"nested\"'));
+ });
+
+ test('prefix class header', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Foobar',
+ fields: [Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@interface ABCFoobar'));
+ });
+
+ test('prefix class source', () {
+ final Root root = Root(apis: [], classes: [
+ Class(
+ name: 'Foobar',
+ fields: [Field(name: 'field1', dataType: 'String')]),
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('@implementation ABCFoobar'));
+ });
+
+ test('prefix nested class header', () {
+ final Root root = Root(apis: [
+ Api(name: 'Api', location: ApiLocation.host, methods: [
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+ ])
+ ], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: [Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcHeader(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, matches('property.*ABCInput'));
+ expect(code, matches('ABCNested.*doSomething.*ABCInput'));
+ expect(code, contains('@protocol ABCApi'));
+ });
+
+ test('prefix nested class source', () {
+ final Root root = Root(apis: [
+ Api(name: 'Api', location: ApiLocation.host, methods: [
+ Method(name: 'doSomething', argType: 'Input', returnType: 'Nested')
+ ])
+ ], classes: [
+ Class(
+ name: 'Input',
+ fields: [Field(name: 'input', dataType: 'String')]),
+ Class(
+ name: 'Nested',
+ fields: [Field(name: 'nested', dataType: 'Input')])
+ ]);
+ final StringBuffer sink = StringBuffer();
+ generateObjcSource(ObjcOptions(prefix: 'ABC'), root, sink);
+ final String code = sink.toString();
+ expect(code, contains('ABCInput fromMap'));
+ expect(code, matches('ABCInput.*=.*ABCInput fromMap'));
+ expect(code, contains('void ABCApiSetup('));
+ });
+}
diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart
new file mode 100644
index 000000000000..1642e9e807b4
--- /dev/null
+++ b/packages/pigeon/test/pigeon_lib_test.dart
@@ -0,0 +1,122 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:pigeon/pigeon_lib.dart';
+import 'package:pigeon/ast.dart';
+
+class Input1 {
+ String input;
+}
+
+class Output1 {
+ String output;
+}
+
+@HostApi()
+abstract class Api1 {
+ Output1 doit(Input1 input);
+}
+
+class InvalidDatatype {
+ dynamic something;
+}
+
+@HostApi()
+abstract class ApiTwoMethods {
+ Output1 method1(Input1 input);
+ Output1 method2(Input1 input);
+}
+
+class Nested {
+ Input1 input;
+}
+
+void main() {
+ test('parse args - input', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(['--input', 'foo.dart']);
+ expect(opts.input, equals('foo.dart'));
+ });
+
+ test('parse args - dart_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(['--dart_out', 'foo.dart']);
+ expect(opts.dartOut, equals('foo.dart'));
+ });
+
+ test('parse args - objc_header_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(['--objc_header_out', 'foo.h']);
+ expect(opts.objcHeaderOut, equals('foo.h'));
+ });
+
+ test('parse args - objc_source_out', () {
+ final PigeonOptions opts =
+ Pigeon.parseArgs(['--objc_source_out', 'foo.m']);
+ expect(opts.objcSourceOut, equals('foo.m'));
+ });
+
+ test('simple parse api', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults parseResult = dartle.parse([Api1]);
+ expect(parseResult.errors.length, equals(0));
+ final Root root = parseResult.root;
+ expect(root.classes.length, equals(2));
+ expect(root.apis.length, equals(1));
+ expect(root.apis[0].name, equals('Api1'));
+ expect(root.apis[0].methods.length, equals(1));
+ expect(root.apis[0].methods[0].name, equals('doit'));
+ expect(root.apis[0].methods[0].argType, equals('Input1'));
+ expect(root.apis[0].methods[0].returnType, equals('Output1'));
+
+ Class input;
+ Class output;
+ for (Class klass in root.classes) {
+ if (klass.name == 'Input1') {
+ input = klass;
+ } else if (klass.name == 'Output1') {
+ output = klass;
+ }
+ }
+ expect(input, isNotNull);
+ expect(output, isNotNull);
+
+ expect(input.fields.length, equals(1));
+ expect(input.fields[0].name, equals('input'));
+ expect(input.fields[0].dataType, equals('String'));
+
+ expect(output.fields.length, equals(1));
+ expect(output.fields[0].name, equals('output'));
+ expect(output.fields[0].dataType, equals('String'));
+ });
+
+ test('invalid datatype', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse([InvalidDatatype]);
+ expect(results.errors.length, 1);
+ expect(results.errors[0].message, contains('InvalidDatatype'));
+ expect(results.errors[0].message, contains('dynamic'));
+ });
+
+ test('two methods', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse([ApiTwoMethods]);
+ expect(results.errors.length, 0);
+ expect(results.root.apis.length, 1);
+ expect(results.root.apis[0].methods.length, equals(2));
+ expect(results.root.apis[0].methods[0].name, equals('method1'));
+ expect(results.root.apis[0].methods[1].name, equals('method2'));
+ });
+
+ test('nested', () {
+ final Pigeon dartle = Pigeon.setup();
+ final ParseResults results = dartle.parse([Nested, Input1]);
+ expect(results.errors.length, equals(0));
+ expect(results.root.classes.length, equals(2));
+ expect(results.root.classes[0].name, equals('Nested'));
+ expect(results.root.classes[0].fields.length, equals(1));
+ expect(results.root.classes[0].fields[0].dataType, equals('Input1'));
+ });
+}