blob: 854c2c835f2f6e61f561fcd4f0907d8471b76c04 [file] [log] [blame]
// Copyright 2013 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 'functional.dart';
import 'generator.dart';
import 'generator_tools.dart';
/// Documentation comment open symbol.
const String _docCommentPrefix = '///';
/// Documentation comment spec.
const DocumentCommentSpecification _docCommentSpec =
DocumentCommentSpecification(_docCommentPrefix);
/// Options that control how Swift code will be generated.
class SwiftOptions {
/// Creates a [SwiftOptions] object
const SwiftOptions({
this.copyrightHeader,
});
/// A copyright header that will get prepended to generated code.
final Iterable<String>? copyrightHeader;
/// Creates a [SwiftOptions] from a Map representation where:
/// `x = SwiftOptions.fromList(x.toMap())`.
static SwiftOptions fromList(Map<String, Object> map) {
return SwiftOptions(
copyrightHeader: map['copyrightHeader'] as Iterable<String>?,
);
}
/// Converts a [SwiftOptions] to a Map representation where:
/// `x = SwiftOptions.fromList(x.toMap())`.
Map<String, Object> toMap() {
final Map<String, Object> result = <String, Object>{
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
};
return result;
}
/// Overrides any non-null parameters from [options] into this to make a new
/// [SwiftOptions].
SwiftOptions merge(SwiftOptions options) {
return SwiftOptions.fromList(mergeMaps(toMap(), options.toMap()));
}
}
/// Class that manages all Swift code generation.
class SwiftGenerator extends StructuredGenerator<SwiftOptions> {
/// Instantiates a Swift Generator.
const SwiftGenerator();
@override
void writeFilePrologue(
SwiftOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
if (generatorOptions.copyrightHeader != null) {
addLines(indent, generatorOptions.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// ${getGeneratedCodeWarning()}');
indent.writeln('// $seeAlsoWarning');
indent.newln();
}
@override
void writeFileImports(
SwiftOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
indent.writeln('import Foundation');
indent.newln();
indent.format('''
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif''');
}
@override
void writeEnum(
SwiftOptions generatorOptions,
Root root,
Indent indent,
Enum anEnum, {
required String dartPackageName,
}) {
indent.newln();
addDocumentationComments(
indent, anEnum.documentationComments, _docCommentSpec);
indent.write('enum ${anEnum.name}: Int ');
indent.addScoped('{', '}', () {
enumerate(anEnum.members, (int index, final EnumMember member) {
addDocumentationComments(
indent, member.documentationComments, _docCommentSpec);
indent.writeln('case ${_camelCase(member.name)} = $index');
});
});
}
@override
void writeDataClass(
SwiftOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
const List<String> generatedComments = <String>[
' Generated class from Pigeon that represents data sent in messages.'
];
indent.newln();
addDocumentationComments(
indent, classDefinition.documentationComments, _docCommentSpec,
generatorComments: generatedComments);
if (classDefinition.isSwiftClass) {
indent.write('class ${classDefinition.name} ');
} else {
indent.write('struct ${classDefinition.name} ');
}
indent.addScoped('{', '}', () {
final Iterable<NamedType> fields =
getFieldsInSerializationOrder(classDefinition);
if (classDefinition.isSwiftClass) {
_writeClassInit(indent, fields.toList());
}
for (final NamedType field in fields) {
addDocumentationComments(
indent, field.documentationComments, _docCommentSpec);
indent.write('var ');
_writeClassField(indent, field, addNil: !classDefinition.isSwiftClass);
indent.newln();
}
indent.newln();
writeClassDecode(
generatorOptions,
root,
indent,
classDefinition,
dartPackageName: dartPackageName,
);
writeClassEncode(
generatorOptions,
root,
indent,
classDefinition,
dartPackageName: dartPackageName,
);
});
}
void _writeClassInit(Indent indent, List<NamedType> fields) {
indent.writeScoped('init(', ')', () {
for (int i = 0; i < fields.length; i++) {
indent.write('');
_writeClassField(indent, fields[i]);
if (i == fields.length - 1) {
indent.newln();
} else {
indent.addln(',');
}
}
}, addTrailingNewline: false);
indent.addScoped(' {', '}', () {
for (final NamedType field in fields) {
_writeClassFieldInit(indent, field);
}
});
}
void _writeClassField(Indent indent, NamedType field, {bool addNil = true}) {
indent.add('${field.name}: ${_nullsafeSwiftTypeForDartType(field.type)}');
final String defaultNil = field.type.isNullable && addNil ? ' = nil' : '';
indent.add(defaultNil);
}
void _writeClassFieldInit(Indent indent, NamedType field) {
indent.writeln('self.${field.name} = ${field.name}');
}
@override
void writeClassEncode(
SwiftOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
indent.write('func toList() -> [Any?] ');
indent.addScoped('{', '}', () {
indent.write('return ');
indent.addScoped('[', ']', () {
// Follow swift-format style, which is to use a trailing comma unless
// there is only one element.
final String separator = classDefinition.fields.length > 1 ? ',' : '';
for (final NamedType field
in getFieldsInSerializationOrder(classDefinition)) {
String toWriteValue = '';
final String fieldName = field.name;
final String nullsafe = field.type.isNullable ? '?' : '';
if (field.type.isClass) {
toWriteValue = '$fieldName$nullsafe.toList()';
} else if (field.type.isEnum) {
toWriteValue = '$fieldName$nullsafe.rawValue';
} else {
toWriteValue = field.name;
}
indent.writeln('$toWriteValue$separator');
}
});
});
}
@override
void writeClassDecode(
SwiftOptions generatorOptions,
Root root,
Indent indent,
Class classDefinition, {
required String dartPackageName,
}) {
final String className = classDefinition.name;
indent.write('static func fromList(_ list: [Any?]) -> $className? ');
indent.addScoped('{', '}', () {
enumerate(getFieldsInSerializationOrder(classDefinition),
(int index, final NamedType field) {
final String listValue = 'list[$index]';
_writeDecodeCasting(
indent: indent,
value: listValue,
variableName: field.name,
type: field.type,
);
});
indent.newln();
indent.write('return ');
indent.addScoped('$className(', ')', () {
for (final NamedType field
in getFieldsInSerializationOrder(classDefinition)) {
final String comma =
getFieldsInSerializationOrder(classDefinition).last == field
? ''
: ',';
indent.writeln('${field.name}: ${field.name}$comma');
}
});
});
}
@override
void writeApis(
SwiftOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
if (root.apis.any((Api api) =>
api is AstHostApi &&
api.methods.any((Method it) => it.isAsynchronous))) {
indent.newln();
}
super.writeApis(generatorOptions, root, indent,
dartPackageName: dartPackageName);
}
/// Writes the code for a flutter [Api], [api].
/// Example:
/// class Foo {
/// private let binaryMessenger: FlutterBinaryMessenger
/// init(binaryMessenger: FlutterBinaryMessenger) {...}
/// func add(x: Int32, y: Int32, completion: @escaping (Int32?) -> Void) {...}
/// }
@override
void writeFlutterApi(
SwiftOptions generatorOptions,
Root root,
Indent indent,
AstFlutterApi api, {
required String dartPackageName,
}) {
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
if (isCustomCodec) {
_writeCodec(indent, api, root);
}
const List<String> generatedComments = <String>[
' Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.'
];
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
generatorComments: generatedComments);
indent.addScoped('protocol ${api.name}Protocol {', '}', () {
for (final Method func in api.methods) {
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
indent.writeln(_getMethodSignature(
name: func.name,
parameters: func.parameters,
returnType: func.returnType,
errorTypeName: 'FlutterError',
isAsynchronous: true,
swiftFunction: func.swiftFunction,
getParameterName: _getSafeArgumentName,
));
}
});
indent.write('class ${api.name}: ${api.name}Protocol ');
indent.addScoped('{', '}', () {
indent.writeln('private let binaryMessenger: FlutterBinaryMessenger');
indent.writeln('private let messageChannelSuffix: String');
indent.write(
'init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") ');
indent.addScoped('{', '}', () {
indent.writeln('self.binaryMessenger = binaryMessenger');
indent.writeln(
r'self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""');
});
final String codecName = _getCodecName(api);
String codecArgumentString = '';
if (getCodecClasses(api, root).isNotEmpty) {
codecArgumentString = ', codec: codec';
indent.write('var codec: FlutterStandardMessageCodec ');
indent.addScoped('{', '}', () {
indent.writeln('return $codecName.shared');
});
}
for (final Method func in api.methods) {
addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);
_writeFlutterMethod(
indent,
name: func.name,
channelName: makeChannelName(api, func, dartPackageName),
parameters: func.parameters,
returnType: func.returnType,
codecArgumentString: codecArgumentString,
swiftFunction: func.swiftFunction,
);
}
});
}
/// Write the swift code that represents a host [Api], [api].
/// Example:
/// protocol Foo {
/// Int32 add(x: Int32, y: Int32)
/// }
@override
void writeHostApi(
SwiftOptions generatorOptions,
Root root,
Indent indent,
AstHostApi api, {
required String dartPackageName,
}) {
final String apiName = api.name;
final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty;
if (isCustomCodec) {
_writeCodec(indent, api, root);
}
const List<String> generatedComments = <String>[
' Generated protocol from Pigeon that represents a handler of messages from Flutter.'
];
addDocumentationComments(indent, api.documentationComments, _docCommentSpec,
generatorComments: generatedComments);
indent.write('protocol $apiName ');
indent.addScoped('{', '}', () {
for (final Method method in api.methods) {
addDocumentationComments(
indent, method.documentationComments, _docCommentSpec);
indent.writeln(_getMethodSignature(
name: method.name,
parameters: method.parameters,
returnType: method.returnType,
errorTypeName: 'Error',
isAsynchronous: method.isAsynchronous,
swiftFunction: method.swiftFunction,
));
}
});
indent.newln();
indent.writeln(
'$_docCommentPrefix Generated setup class from Pigeon to handle messages through the `binaryMessenger`.');
indent.write('class ${apiName}Setup ');
indent.addScoped('{', '}', () {
final String codecName = _getCodecName(api);
indent.writeln('$_docCommentPrefix The codec used by $apiName.');
String codecArgumentString = '';
if (getCodecClasses(api, root).isNotEmpty) {
codecArgumentString = ', codec: codec';
indent.writeln(
'static var codec: FlutterStandardMessageCodec { $codecName.shared }');
}
indent.writeln(
'$_docCommentPrefix Sets up an instance of `$apiName` to handle messages through the `binaryMessenger`.');
indent.write(
'static func setUp(binaryMessenger: FlutterBinaryMessenger, api: $apiName?, messageChannelSuffix: String = "") ');
indent.addScoped('{', '}', () {
indent.writeln(
r'let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""');
for (final Method method in api.methods) {
_writeHostMethodMessageHandler(
indent,
name: method.name,
channelName: makeChannelName(api, method, dartPackageName),
parameters: method.parameters,
returnType: method.returnType,
isAsynchronous: method.isAsynchronous,
codecArgumentString: codecArgumentString,
swiftFunction: method.swiftFunction,
documentationComments: method.documentationComments,
);
}
});
});
}
/// Writes the codec class will be used for encoding messages for the [api].
/// Example:
/// private class FooHostApiCodecReader: FlutterStandardReader {...}
/// private class FooHostApiCodecWriter: FlutterStandardWriter {...}
/// private class FooHostApiCodecReaderWriter: FlutterStandardReaderWriter {...}
void _writeCodec(Indent indent, Api api, Root root) {
assert(getCodecClasses(api, root).isNotEmpty);
final String codecName = _getCodecName(api);
final String readerWriterName = '${codecName}ReaderWriter';
final String readerName = '${codecName}Reader';
final String writerName = '${codecName}Writer';
// Generate Reader
indent.write('private class $readerName: FlutterStandardReader ');
indent.addScoped('{', '}', () {
if (getCodecClasses(api, root).isNotEmpty) {
indent.write('override func readValue(ofType type: UInt8) -> Any? ');
indent.addScoped('{', '}', () {
indent.write('switch type ');
indent.addScoped('{', '}', nestCount: 0, () {
for (final EnumeratedClass customClass
in getCodecClasses(api, root)) {
indent.writeln('case ${customClass.enumeration}:');
indent.nest(1, () {
indent.writeln(
'return ${customClass.name}.fromList(self.readValue() as! [Any?])');
});
}
indent.writeln('default:');
indent.nest(1, () {
indent.writeln('return super.readValue(ofType: type)');
});
});
});
}
});
// Generate Writer
indent.newln();
indent.write('private class $writerName: FlutterStandardWriter ');
indent.addScoped('{', '}', () {
if (getCodecClasses(api, root).isNotEmpty) {
indent.write('override func writeValue(_ value: Any) ');
indent.addScoped('{', '}', () {
indent.write('');
for (final EnumeratedClass customClass
in getCodecClasses(api, root)) {
indent.add('if let value = value as? ${customClass.name} ');
indent.addScoped('{', '} else ', () {
indent.writeln('super.writeByte(${customClass.enumeration})');
indent.writeln('super.writeValue(value.toList())');
}, addTrailingNewline: false);
}
indent.addScoped('{', '}', () {
indent.writeln('super.writeValue(value)');
});
});
}
});
indent.newln();
// Generate ReaderWriter
indent
.write('private class $readerWriterName: FlutterStandardReaderWriter ');
indent.addScoped('{', '}', () {
indent.write(
'override func reader(with data: Data) -> FlutterStandardReader ');
indent.addScoped('{', '}', () {
indent.writeln('return $readerName(data: data)');
});
indent.newln();
indent.write(
'override func writer(with data: NSMutableData) -> FlutterStandardWriter ');
indent.addScoped('{', '}', () {
indent.writeln('return $writerName(data: data)');
});
});
indent.newln();
// Generate Codec
indent.write('class $codecName: FlutterStandardMessageCodec ');
indent.addScoped('{', '}', () {
indent.writeln(
'static let shared = $codecName(readerWriter: $readerWriterName())');
});
indent.newln();
}
String _castForceUnwrap(String value, TypeDeclaration type) {
assert(!type.isVoid);
if (type.isEnum) {
String output =
'${_swiftTypeForDartType(type)}(rawValue: $value as! Int)!';
if (type.isNullable) {
output = 'isNullish($value) ? nil : $output';
}
return output;
} else if (type.baseName == 'Object') {
return value + (type.isNullable ? '' : '!');
} else if (type.baseName == 'int') {
if (type.isNullable) {
// Nullable ints need to check for NSNull, and Int32 before casting can be done safely.
// This nested ternary is a necessary evil to avoid less efficient conversions.
return 'isNullish($value) ? nil : ($value is Int64? ? $value as! Int64? : Int64($value as! Int32))';
} else {
return '$value is Int64 ? $value as! Int64 : Int64($value as! Int32)';
}
} else if (type.isNullable) {
return 'nilOrValue($value)';
} else {
return '$value as! ${_swiftTypeForDartType(type)}';
}
}
void _writeGenericCasting({
required Indent indent,
required String value,
required String variableName,
required String fieldType,
required TypeDeclaration type,
}) {
if (type.isNullable) {
indent.writeln(
'let $variableName: $fieldType? = ${_castForceUnwrap(value, type)}');
} else {
indent.writeln('let $variableName = ${_castForceUnwrap(value, type)}');
}
}
/// Writes decode and casting code for any type.
///
/// Optional parameters should only be used for class decoding.
void _writeDecodeCasting({
required Indent indent,
required String value,
required String variableName,
required TypeDeclaration type,
}) {
final String fieldType = _swiftTypeForDartType(type);
if (type.isNullable) {
if (type.isClass) {
indent.writeln('var $variableName: $fieldType? = nil');
indent
.write('if let ${variableName}List: [Any?] = nilOrValue($value) ');
indent.addScoped('{', '}', () {
indent.writeln(
'$variableName = $fieldType.fromList(${variableName}List)');
});
} else if (type.isEnum) {
indent.writeln('var $variableName: $fieldType? = nil');
indent.writeln(
'let ${variableName}EnumVal: Int? = ${_castForceUnwrap(value, const TypeDeclaration(baseName: 'Int', isNullable: true))}');
indent
.write('if let ${variableName}RawValue = ${variableName}EnumVal ');
indent.addScoped('{', '}', () {
indent.writeln(
'$variableName = $fieldType(rawValue: ${variableName}RawValue)!');
});
} else {
_writeGenericCasting(
indent: indent,
value: value,
variableName: variableName,
fieldType: fieldType,
type: type,
);
}
} else {
if (type.isClass) {
indent.writeln(
'let $variableName = $fieldType.fromList($value as! [Any?])!');
} else {
_writeGenericCasting(
indent: indent,
value: value,
variableName: variableName,
fieldType: fieldType,
type: type,
);
}
}
}
void _writeIsNullish(Indent indent) {
indent.newln();
indent.write('private func isNullish(_ value: Any?) -> Bool ');
indent.addScoped('{', '}', () {
indent.writeln('return value is NSNull || value == nil');
});
}
void _writeWrapResult(Indent indent) {
indent.newln();
indent.write('private func wrapResult(_ result: Any?) -> [Any?] ');
indent.addScoped('{', '}', () {
indent.writeln('return [result]');
});
}
void _writeWrapError(Indent indent) {
indent.newln();
indent.write('private func wrapError(_ error: Any) -> [Any?] ');
indent.addScoped('{', '}', () {
indent.write('if let flutterError = error as? FlutterError ');
indent.addScoped('{', '}', () {
indent.write('return ');
indent.addScoped('[', ']', () {
indent.writeln('flutterError.code,');
indent.writeln('flutterError.message,');
indent.writeln('flutterError.details,');
});
});
indent.write('return ');
indent.addScoped('[', ']', () {
indent.writeln(r'"\(error)",');
indent.writeln(r'"\(type(of: error))",');
indent.writeln(r'"Stacktrace: \(Thread.callStackSymbols)",');
});
});
}
void _writeNilOrValue(Indent indent) {
indent.format('''
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}''');
}
void _writeCreateConnectionError(Indent indent) {
indent.newln();
indent.writeScoped(
'private func createConnectionError(withChannelName channelName: String) -> FlutterError {',
'}', () {
indent.writeln(
'return FlutterError(code: "channel-error", message: "Unable to establish connection on channel: \'\\(channelName)\'.", details: "")');
});
}
@override
void writeGeneralUtilities(
SwiftOptions generatorOptions,
Root root,
Indent indent, {
required String dartPackageName,
}) {
final bool hasHostApi = root.apis
.whereType<AstHostApi>()
.any((Api api) => api.methods.isNotEmpty);
final bool hasFlutterApi = root.apis
.whereType<AstFlutterApi>()
.any((Api api) => api.methods.isNotEmpty);
if (hasHostApi) {
_writeWrapResult(indent);
_writeWrapError(indent);
}
if (hasFlutterApi) {
_writeCreateConnectionError(indent);
}
_writeIsNullish(indent);
_writeNilOrValue(indent);
}
void _writeFlutterMethod(
Indent indent, {
required String name,
required String channelName,
required List<Parameter> parameters,
required TypeDeclaration returnType,
required String codecArgumentString,
required String? swiftFunction,
}) {
final String methodSignature = _getMethodSignature(
name: name,
parameters: parameters,
returnType: returnType,
errorTypeName: 'FlutterError',
isAsynchronous: true,
swiftFunction: swiftFunction,
getParameterName: _getSafeArgumentName,
);
/// Returns an argument name that can be used in a context where it is possible to collide.
String getEnumSafeArgumentExpression(int count, NamedType argument) {
String enumTag = '';
if (argument.type.isEnum) {
enumTag = argument.type.isNullable ? '?.rawValue' : '.rawValue';
}
return '${_getArgumentName(count, argument)}Arg$enumTag';
}
indent.writeScoped('$methodSignature {', '}', () {
final Iterable<String> enumSafeArgNames = parameters.asMap().entries.map(
(MapEntry<int, NamedType> e) =>
getEnumSafeArgumentExpression(e.key, e.value));
final String sendArgument = parameters.isEmpty
? 'nil'
: '[${enumSafeArgNames.join(', ')}] as [Any?]';
const String channel = 'channel';
indent.writeln(
'let channelName: String = "$channelName\\(messageChannelSuffix)"');
indent.writeln(
'let $channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger$codecArgumentString)');
indent.write('$channel.sendMessage($sendArgument) ');
indent.addScoped('{ response in', '}', () {
indent.writeScoped(
'guard let listResponse = response as? [Any?] else {', '}', () {
indent.writeln(
'completion(.failure(createConnectionError(withChannelName: channelName)))');
indent.writeln('return');
});
indent.writeScoped('if listResponse.count > 1 {', '} ', () {
indent.writeln('let code: String = listResponse[0] as! String');
indent.writeln('let message: String? = nilOrValue(listResponse[1])');
indent.writeln('let details: String? = nilOrValue(listResponse[2])');
indent.writeln(
'completion(.failure(FlutterError(code: code, message: message, details: details)))');
}, addTrailingNewline: false);
if (!returnType.isNullable && !returnType.isVoid) {
indent.addScoped('else if listResponse[0] == nil {', '} ', () {
indent.writeln(
'completion(.failure(FlutterError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))');
}, addTrailingNewline: false);
}
indent.addScoped('else {', '}', () {
if (returnType.isVoid) {
indent.writeln('completion(.success(Void()))');
} else {
final String fieldType = _swiftTypeForDartType(returnType);
_writeGenericCasting(
indent: indent,
value: 'listResponse[0]',
variableName: 'result',
fieldType: fieldType,
type: returnType,
);
indent.writeln('completion(.success(result))');
}
});
});
});
}
void _writeHostMethodMessageHandler(
Indent indent, {
required String name,
required String channelName,
required Iterable<Parameter> parameters,
required TypeDeclaration returnType,
required bool isAsynchronous,
required String codecArgumentString,
required String? swiftFunction,
List<String> documentationComments = const <String>[],
}) {
final _SwiftFunctionComponents components = _SwiftFunctionComponents(
name: name,
parameters: parameters,
returnType: returnType,
swiftFunction: swiftFunction,
);
final String varChannelName = '${name}Channel';
addDocumentationComments(indent, documentationComments, _docCommentSpec);
indent.writeln(
'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName\\(channelSuffix)", binaryMessenger: binaryMessenger$codecArgumentString)');
indent.write('if let api = api ');
indent.addScoped('{', '}', () {
indent.write('$varChannelName.setMessageHandler ');
final String messageVarName = parameters.isNotEmpty ? 'message' : '_';
indent.addScoped('{ $messageVarName, reply in', '}', () {
final List<String> methodArgument = <String>[];
if (components.arguments.isNotEmpty) {
indent.writeln('let args = message as! [Any?]');
enumerate(components.arguments,
(int index, _SwiftFunctionArgument arg) {
final String argName = _getSafeArgumentName(index, arg.namedType);
final String argIndex = 'args[$index]';
final String fieldType = _swiftTypeForDartType(arg.type);
_writeGenericCasting(
indent: indent,
value: argIndex,
variableName: argName,
fieldType: fieldType,
type: arg.type);
if (arg.label == '_') {
methodArgument.add(argName);
} else {
methodArgument.add('${arg.label ?? arg.name}: $argName');
}
});
}
final String tryStatement = isAsynchronous ? '' : 'try ';
// Empty parens are not required when calling a method whose only
// argument is a trailing closure.
final String argumentString = methodArgument.isEmpty && isAsynchronous
? ''
: '(${methodArgument.join(', ')})';
final String call =
'${tryStatement}api.${components.name}$argumentString';
if (isAsynchronous) {
final String resultName = returnType.isVoid ? 'nil' : 'res';
final String successVariableInit =
returnType.isVoid ? '' : '(let res)';
indent.write('$call ');
indent.addScoped('{ result in', '}', () {
indent.write('switch result ');
indent.addScoped('{', '}', nestCount: 0, () {
final String nullsafe = returnType.isNullable ? '?' : '';
final String enumTag =
returnType.isEnum ? '$nullsafe.rawValue' : '';
indent.writeln('case .success$successVariableInit:');
indent.nest(1, () {
indent.writeln('reply(wrapResult($resultName$enumTag))');
});
indent.writeln('case .failure(let error):');
indent.nest(1, () {
indent.writeln('reply(wrapError(error))');
});
});
});
} else {
indent.write('do ');
indent.addScoped('{', '}', () {
if (returnType.isVoid) {
indent.writeln(call);
indent.writeln('reply(wrapResult(nil))');
} else {
String enumTag = '';
if (returnType.isEnum) {
enumTag = '.rawValue';
}
enumTag = returnType.isNullable && returnType.isEnum
? '?$enumTag'
: enumTag;
indent.writeln('let result = $call');
indent.writeln('reply(wrapResult(result$enumTag))');
}
}, addTrailingNewline: false);
indent.addScoped(' catch {', '}', () {
indent.writeln('reply(wrapError(error))');
});
}
});
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
indent.writeln('$varChannelName.setMessageHandler(nil)');
});
}
}
/// Calculates the name of the codec that will be generated for [api].
String _getCodecName(Api api) => '${api.name}Codec';
String _getArgumentName(int count, NamedType argument) =>
argument.name.isEmpty ? 'arg$count' : argument.name;
/// Returns an argument name that can be used in a context where it is possible to collide.
String _getSafeArgumentName(int count, NamedType argument) =>
'${_getArgumentName(count, argument)}Arg';
String _camelCase(String text) {
final String pascal = text.split('_').map((String part) {
return part.isEmpty ? '' : part[0].toUpperCase() + part.substring(1);
}).join();
return pascal[0].toLowerCase() + pascal.substring(1);
}
/// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
/// used in Swift code.
String _flattenTypeArguments(List<TypeDeclaration> args) {
return args.map((TypeDeclaration e) => _swiftTypeForDartType(e)).join(', ');
}
String _swiftTypeForBuiltinGenericDartType(TypeDeclaration type) {
if (type.typeArguments.isEmpty) {
if (type.baseName == 'List') {
return '[Any?]';
} else if (type.baseName == 'Map') {
return '[AnyHashable: Any?]';
} else {
return 'Any';
}
} else {
if (type.baseName == 'List') {
return '[${_nullsafeSwiftTypeForDartType(type.typeArguments.first)}]';
} else if (type.baseName == 'Map') {
return '[${_nullsafeSwiftTypeForDartType(type.typeArguments.first)}: ${_nullsafeSwiftTypeForDartType(type.typeArguments.last)}]';
} else {
return '${type.baseName}<${_flattenTypeArguments(type.typeArguments)}>';
}
}
}
String? _swiftTypeForBuiltinDartType(TypeDeclaration type) {
const Map<String, String> swiftTypeForDartTypeMap = <String, String>{
'void': 'Void',
'bool': 'Bool',
'String': 'String',
'int': 'Int64',
'double': 'Double',
'Uint8List': 'FlutterStandardTypedData',
'Int32List': 'FlutterStandardTypedData',
'Int64List': 'FlutterStandardTypedData',
'Float32List': 'FlutterStandardTypedData',
'Float64List': 'FlutterStandardTypedData',
'Object': 'Any',
};
if (swiftTypeForDartTypeMap.containsKey(type.baseName)) {
return swiftTypeForDartTypeMap[type.baseName];
} else if (type.baseName == 'List' || type.baseName == 'Map') {
return _swiftTypeForBuiltinGenericDartType(type);
} else {
return null;
}
}
String _swiftTypeForDartType(TypeDeclaration type) {
return _swiftTypeForBuiltinDartType(type) ?? type.baseName;
}
String _nullsafeSwiftTypeForDartType(TypeDeclaration type) {
final String nullSafe = type.isNullable ? '?' : '';
return '${_swiftTypeForDartType(type)}$nullSafe';
}
String _getMethodSignature({
required String name,
required Iterable<Parameter> parameters,
required TypeDeclaration returnType,
required String errorTypeName,
bool isAsynchronous = false,
String? swiftFunction,
String Function(int index, NamedType argument) getParameterName =
_getArgumentName,
}) {
final _SwiftFunctionComponents components = _SwiftFunctionComponents(
name: name,
parameters: parameters,
returnType: returnType,
swiftFunction: swiftFunction,
);
final String returnTypeString =
returnType.isVoid ? 'Void' : _nullsafeSwiftTypeForDartType(returnType);
final Iterable<String> types =
parameters.map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type));
final Iterable<String> labels = indexMap(components.arguments,
(int index, _SwiftFunctionArgument argument) {
return argument.label ?? _getArgumentName(index, argument.namedType);
});
final Iterable<String> names = indexMap(parameters, getParameterName);
final String parameterSignature =
map3(types, labels, names, (String type, String label, String name) {
return '${label != name ? '$label ' : ''}$name: $type';
}).join(', ');
if (isAsynchronous) {
if (parameters.isEmpty) {
return 'func ${components.name}(completion: @escaping (Result<$returnTypeString, $errorTypeName>) -> Void)';
} else {
return 'func ${components.name}($parameterSignature, completion: @escaping (Result<$returnTypeString, $errorTypeName>) -> Void)';
}
} else {
if (returnType.isVoid) {
return 'func ${components.name}($parameterSignature) throws';
} else {
return 'func ${components.name}($parameterSignature) throws -> $returnTypeString';
}
}
}
/// A class that represents a Swift function argument.
///
/// The [name] is the name of the argument.
/// The [type] is the type of the argument.
/// The [namedType] is the [NamedType] that this argument is generated from.
/// The [label] is the label of the argument.
class _SwiftFunctionArgument {
_SwiftFunctionArgument({
required this.name,
required this.type,
required this.namedType,
this.label,
});
final String name;
final TypeDeclaration type;
final NamedType namedType;
final String? label;
}
/// A class that represents a Swift function signature.
///
/// The [name] is the name of the function.
/// The [arguments] are the arguments of the function.
/// The [returnType] is the return type of the function.
/// The [method] is the method that this function signature is generated from.
class _SwiftFunctionComponents {
/// Constructor that generates a [_SwiftFunctionComponents] from a [Method].
factory _SwiftFunctionComponents({
required String name,
required Iterable<Parameter> parameters,
required TypeDeclaration returnType,
String? swiftFunction,
}) {
if (swiftFunction == null || swiftFunction.isEmpty) {
return _SwiftFunctionComponents._(
name: name,
returnType: returnType,
arguments: parameters
.map((NamedType field) => _SwiftFunctionArgument(
name: field.name,
type: field.type,
namedType: field,
))
.toList(),
);
}
final String argsExtractor = repeat(r'(\w+):', parameters.length).join();
final RegExp signatureRegex = RegExp(r'(\w+) *\(' + argsExtractor + r'\)');
final RegExpMatch match = signatureRegex.firstMatch(swiftFunction)!;
final Iterable<String> labels = match
.groups(List<int>.generate(parameters.length, (int index) => index + 2))
.whereType();
return _SwiftFunctionComponents._(
name: match.group(1)!,
returnType: returnType,
arguments: map2(
parameters,
labels,
(NamedType field, String label) => _SwiftFunctionArgument(
name: field.name,
label: label == field.name ? null : label,
type: field.type,
namedType: field,
),
).toList(),
);
}
_SwiftFunctionComponents._({
required this.name,
required this.arguments,
required this.returnType,
});
final String name;
final List<_SwiftFunctionArgument> arguments;
final TypeDeclaration returnType;
}