| // 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.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 klass, { |
| required String dartPackageName, |
| }) { |
| final Set<String> customClassNames = |
| root.classes.map((Class x) => x.name).toSet(); |
| final Set<String> customEnumNames = |
| root.enums.map((Enum x) => x.name).toSet(); |
| |
| const List<String> generatedComments = <String>[ |
| ' Generated class from Pigeon that represents data sent in messages.' |
| ]; |
| indent.newln(); |
| addDocumentationComments( |
| indent, klass.documentationComments, _docCommentSpec, |
| generatorComments: generatedComments); |
| |
| indent.write('struct ${klass.name} '); |
| indent.addScoped('{', '}', () { |
| getFieldsInSerializationOrder(klass).forEach((NamedType field) { |
| _writeClassField(indent, field); |
| }); |
| |
| indent.newln(); |
| writeClassDecode( |
| generatorOptions, |
| root, |
| indent, |
| klass, |
| customClassNames, |
| customEnumNames, |
| dartPackageName: dartPackageName, |
| ); |
| writeClassEncode( |
| generatorOptions, |
| root, |
| indent, |
| klass, |
| customClassNames, |
| customEnumNames, |
| dartPackageName: dartPackageName, |
| ); |
| }); |
| } |
| |
| @override |
| void writeClassEncode( |
| SwiftOptions generatorOptions, |
| Root root, |
| Indent indent, |
| Class klass, |
| Set<String> customClassNames, |
| Set<String> customEnumNames, { |
| required String dartPackageName, |
| }) { |
| indent.write('func toList() -> [Any?] '); |
| indent.addScoped('{', '}', () { |
| indent.write('return '); |
| indent.addScoped('[', ']', () { |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| final HostDatatype hostDatatype = _getHostDatatype(root, field); |
| String toWriteValue = ''; |
| final String fieldName = field.name; |
| final String nullsafe = field.type.isNullable ? '?' : ''; |
| if (!hostDatatype.isBuiltin && |
| customClassNames.contains(field.type.baseName)) { |
| toWriteValue = '$fieldName$nullsafe.toList()'; |
| } else if (!hostDatatype.isBuiltin && |
| customEnumNames.contains(field.type.baseName)) { |
| toWriteValue = '$fieldName$nullsafe.rawValue'; |
| } else { |
| toWriteValue = field.name; |
| } |
| |
| indent.writeln('$toWriteValue,'); |
| } |
| }); |
| }); |
| } |
| |
| @override |
| void writeClassDecode( |
| SwiftOptions generatorOptions, |
| Root root, |
| Indent indent, |
| Class klass, |
| Set<String> customClassNames, |
| Set<String> customEnumNames, { |
| required String dartPackageName, |
| }) { |
| final String className = klass.name; |
| indent.write('static func fromList(_ list: [Any?]) -> $className? '); |
| |
| indent.addScoped('{', '}', () { |
| enumerate(getFieldsInSerializationOrder(klass), |
| (int index, final NamedType field) { |
| final String listValue = 'list[$index]'; |
| |
| _writeDecodeCasting( |
| root: root, |
| indent: indent, |
| value: listValue, |
| variableName: field.name, |
| type: field.type, |
| listEncodedClassNames: customClassNames, |
| listEncodedEnumNames: customEnumNames, |
| ); |
| }); |
| |
| indent.newln(); |
| indent.write('return '); |
| indent.addScoped('$className(', ')', () { |
| for (final NamedType field in getFieldsInSerializationOrder(klass)) { |
| final String comma = |
| getFieldsInSerializationOrder(klass).last == field ? '' : ','; |
| indent.writeln('${field.name}: ${field.name}$comma'); |
| } |
| }); |
| }); |
| } |
| |
| void _writeClassField(Indent indent, NamedType field) { |
| addDocumentationComments( |
| indent, field.documentationComments, _docCommentSpec); |
| |
| indent.write( |
| 'var ${field.name}: ${_nullsafeSwiftTypeForDartType(field.type)}'); |
| final String defaultNil = field.type.isNullable ? ' = nil' : ''; |
| indent.addln(defaultNil); |
| } |
| |
| @override |
| void writeApis( |
| SwiftOptions generatorOptions, |
| Root root, |
| Indent indent, { |
| required String dartPackageName, |
| }) { |
| if (root.apis.any((Api api) => |
| api.location == ApiLocation.host && |
| 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, |
| Api api, { |
| required String dartPackageName, |
| }) { |
| assert(api.location == ApiLocation.flutter); |
| |
| /// Returns an argument name that can be used in a context where it is possible to collide. |
| String getEnumSafeArgumentExpression( |
| Root root, int count, NamedType argument) { |
| String enumTag = ''; |
| if (isEnum(root, argument.type)) { |
| enumTag = argument.type.isNullable ? '?.rawValue' : '.rawValue'; |
| } |
| |
| return '${_getArgumentName(count, argument)}Arg$enumTag'; |
| } |
| |
| final bool isCustomCodec = getCodecClasses(api, root).isNotEmpty; |
| if (isCustomCodec) { |
| _writeCodec(indent, api, root); |
| } |
| const List<String> generatedComments = <String>[ |
| ' Generated class from Pigeon that represents Flutter messages that can be called from Swift.' |
| ]; |
| addDocumentationComments(indent, api.documentationComments, _docCommentSpec, |
| generatorComments: generatedComments); |
| |
| indent.write('class ${api.name} '); |
| indent.addScoped('{', '}', () { |
| indent.writeln('private let binaryMessenger: FlutterBinaryMessenger'); |
| indent.write('init(binaryMessenger: FlutterBinaryMessenger)'); |
| indent.addScoped('{', '}', () { |
| indent.writeln('self.binaryMessenger = binaryMessenger'); |
| }); |
| 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) { |
| final _SwiftFunctionComponents components = |
| _SwiftFunctionComponents.fromMethod(func); |
| |
| final String channelName = makeChannelName(api, func, dartPackageName); |
| final String returnType = func.returnType.isVoid |
| ? '' |
| : _nullsafeSwiftTypeForDartType(func.returnType); |
| String sendArgument; |
| addDocumentationComments( |
| indent, func.documentationComments, _docCommentSpec); |
| |
| if (func.arguments.isEmpty) { |
| indent.write( |
| 'func ${func.name}(completion: @escaping ($returnType) -> Void) '); |
| sendArgument = 'nil'; |
| } else { |
| final Iterable<String> argTypes = func.arguments |
| .map((NamedType e) => _nullsafeSwiftTypeForDartType(e.type)); |
| final Iterable<String> argLabels = indexMap(components.arguments, |
| (int index, _SwiftFunctionArgument argument) { |
| return argument.label ?? |
| _getArgumentName(index, argument.namedType); |
| }); |
| final Iterable<String> argNames = |
| indexMap(func.arguments, _getSafeArgumentName); |
| final Iterable<String> enumSafeArgNames = func.arguments |
| .asMap() |
| .entries |
| .map((MapEntry<int, NamedType> e) => |
| getEnumSafeArgumentExpression(root, e.key, e.value)); |
| sendArgument = '[${enumSafeArgNames.join(', ')}] as [Any?]'; |
| final String argsSignature = map3( |
| argTypes, |
| argLabels, |
| argNames, |
| (String type, String label, String name) => |
| '$label $name: $type').join(', '); |
| if (func.returnType.isVoid) { |
| indent.write( |
| 'func ${components.name}($argsSignature, completion: @escaping () -> Void) '); |
| } else { |
| indent.write( |
| 'func ${components.name}($argsSignature, completion: @escaping ($returnType) -> Void) '); |
| } |
| } |
| indent.addScoped('{', '}', () { |
| const String channel = 'channel'; |
| indent.writeln( |
| 'let $channel = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)'); |
| indent.write('$channel.sendMessage($sendArgument) '); |
| if (func.returnType.isVoid) { |
| indent.addScoped('{ _ in', '}', () { |
| indent.writeln('completion()'); |
| }); |
| } else { |
| indent.addScoped('{ response in', '}', () { |
| _writeDecodeCasting( |
| root: root, |
| indent: indent, |
| value: 'response', |
| variableName: 'result', |
| type: func.returnType, |
| ); |
| indent.writeln('completion(result)'); |
| }); |
| } |
| }); |
| } |
| }); |
| } |
| |
| /// 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, |
| Api api, { |
| required String dartPackageName, |
| }) { |
| assert(api.location == ApiLocation.host); |
| |
| 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) { |
| final _SwiftFunctionComponents components = |
| _SwiftFunctionComponents.fromMethod(method); |
| final List<String> argSignature = |
| components.arguments.map((_SwiftFunctionArgument argument) { |
| final String? label = argument.label; |
| final String name = argument.name; |
| final String type = _nullsafeSwiftTypeForDartType(argument.type); |
| return '${label == null ? '' : '$label '}$name: $type'; |
| }).toList(); |
| |
| final String returnType = method.returnType.isVoid |
| ? 'Void' |
| : _nullsafeSwiftTypeForDartType(method.returnType); |
| |
| final String escapeType = |
| method.returnType.isVoid ? 'Void' : returnType; |
| |
| addDocumentationComments( |
| indent, method.documentationComments, _docCommentSpec); |
| |
| if (method.isAsynchronous) { |
| argSignature.add( |
| 'completion: @escaping (Result<$escapeType, Error>) -> Void'); |
| indent.writeln('func ${components.name}(${argSignature.join(', ')})'); |
| } else if (method.returnType.isVoid) { |
| indent.writeln( |
| 'func ${components.name}(${argSignature.join(', ')}) throws'); |
| } else { |
| indent.writeln( |
| 'func ${components.name}(${argSignature.join(', ')}) throws -> $returnType'); |
| } |
| } |
| }); |
| |
| 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?) '); |
| indent.addScoped('{', '}', () { |
| for (final Method method in api.methods) { |
| final _SwiftFunctionComponents components = |
| _SwiftFunctionComponents.fromMethod(method); |
| |
| final String channelName = |
| makeChannelName(api, method, dartPackageName); |
| final String varChannelName = '${method.name}Channel'; |
| addDocumentationComments( |
| indent, method.documentationComments, _docCommentSpec); |
| |
| indent.writeln( |
| 'let $varChannelName = FlutterBasicMessageChannel(name: "$channelName", binaryMessenger: binaryMessenger$codecArgumentString)'); |
| indent.write('if let api = api '); |
| indent.addScoped('{', '}', () { |
| indent.write('$varChannelName.setMessageHandler '); |
| final String messageVarName = |
| method.arguments.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]'; |
| _writeDecodeCasting( |
| root: root, |
| indent: indent, |
| value: argIndex, |
| variableName: argName, |
| type: arg.type, |
| ); |
| if (arg.label == '_') { |
| methodArgument.add(argName); |
| } else { |
| methodArgument.add('${arg.label ?? arg.name}: $argName'); |
| } |
| }); |
| } |
| final String tryStatement = method.isAsynchronous ? '' : 'try '; |
| final String call = |
| '${tryStatement}api.${components.name}(${methodArgument.join(', ')})'; |
| if (method.isAsynchronous) { |
| final String resultName = |
| method.returnType.isVoid ? 'nil' : 'res'; |
| final String successVariableInit = |
| method.returnType.isVoid ? '' : '(let res)'; |
| indent.write('$call '); |
| |
| indent.addScoped('{ result in', '}', () { |
| indent.write('switch result '); |
| indent.addScoped('{', '}', () { |
| final String nullsafe = |
| method.returnType.isNullable ? '?' : ''; |
| final String enumTag = isEnum(root, method.returnType) |
| ? '$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 (method.returnType.isVoid) { |
| indent.writeln(call); |
| indent.writeln('reply(wrapResult(nil))'); |
| } else { |
| String enumTag = ''; |
| if (isEnum(root, method.returnType)) { |
| enumTag = '.rawValue'; |
| } |
| enumTag = method.returnType.isNullable && |
| isEnum(root, method.returnType) |
| ? '?$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)'); |
| }); |
| } |
| }); |
| }); |
| } |
| |
| /// 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('{', '}', () { |
| 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(); |
| } |
| |
| /// Writes decode and casting code for any type. |
| /// |
| /// Optional parameters should only be used for class decoding. |
| void _writeDecodeCasting({ |
| required Root root, |
| required Indent indent, |
| required String value, |
| required String variableName, |
| required TypeDeclaration type, |
| Set<String>? listEncodedClassNames, |
| Set<String>? listEncodedEnumNames, |
| }) { |
| String castForceUnwrap(String value, TypeDeclaration type, Root root) { |
| if (isEnum(root, type)) { |
| 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)}'; |
| } |
| } |
| |
| final String fieldType = _swiftTypeForDartType(type); |
| |
| if (type.isNullable) { |
| if (listEncodedClassNames != null && |
| listEncodedClassNames.contains(type.baseName)) { |
| 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 (listEncodedEnumNames != null && |
| listEncodedEnumNames.contains(type.baseName)) { |
| indent.writeln('var $variableName: $fieldType? = nil'); |
| indent.writeln( |
| 'let ${variableName}EnumVal: Int? = ${castForceUnwrap(value, const TypeDeclaration(baseName: 'Int', isNullable: true), root)}'); |
| indent |
| .write('if let ${variableName}RawValue = ${variableName}EnumVal '); |
| indent.addScoped('{', '}', () { |
| indent.writeln( |
| '$variableName = $fieldType(rawValue: ${variableName}RawValue)!'); |
| }); |
| } else { |
| indent.writeln( |
| 'let $variableName: $fieldType? = ${castForceUnwrap(value, type, root)}'); |
| } |
| } else { |
| if (listEncodedClassNames != null && |
| listEncodedClassNames.contains(type.baseName)) { |
| indent.writeln( |
| 'let $variableName = $fieldType.fromList($value as! [Any?])!'); |
| } else { |
| indent.writeln( |
| 'let $variableName = ${castForceUnwrap(value, type, root)}'); |
| } |
| } |
| } |
| |
| 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? |
| }'''); |
| } |
| |
| @override |
| void writeGeneralUtilities( |
| SwiftOptions generatorOptions, |
| Root root, |
| Indent indent, { |
| required String dartPackageName, |
| }) { |
| _writeIsNullish(indent); |
| _writeWrapResult(indent); |
| _writeWrapError(indent); |
| _writeNilOrValue(indent); |
| } |
| } |
| |
| HostDatatype _getHostDatatype(Root root, NamedType field) { |
| return getFieldHostDatatype(field, root.classes, root.enums, |
| (TypeDeclaration x) => _swiftTypeForBuiltinDartType(x)); |
| } |
| |
| /// 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'; |
| } |
| |
| /// 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 { |
| _SwiftFunctionComponents._({ |
| required this.name, |
| required this.arguments, |
| required this.returnType, |
| required this.method, |
| }); |
| |
| /// Constructor that generates a [_SwiftFunctionComponents] from a [Method]. |
| factory _SwiftFunctionComponents.fromMethod(Method method) { |
| if (method.swiftFunction.isEmpty) { |
| return _SwiftFunctionComponents._( |
| name: method.name, |
| returnType: method.returnType, |
| arguments: method.arguments |
| .map((NamedType field) => _SwiftFunctionArgument( |
| name: field.name, |
| type: field.type, |
| namedType: field, |
| )) |
| .toList(), |
| method: method, |
| ); |
| } |
| |
| final String argsExtractor = |
| repeat(r'(\w+):', method.arguments.length).join(); |
| final RegExp signatureRegex = RegExp(r'(\w+) *\(' + argsExtractor + r'\)'); |
| final RegExpMatch match = signatureRegex.firstMatch(method.swiftFunction)!; |
| |
| final Iterable<String> labels = match |
| .groups(List<int>.generate( |
| method.arguments.length, (int index) => index + 2)) |
| .whereType(); |
| |
| return _SwiftFunctionComponents._( |
| name: match.group(1)!, |
| returnType: method.returnType, |
| arguments: map2( |
| method.arguments, |
| labels, |
| (NamedType field, String label) => _SwiftFunctionArgument( |
| name: field.name, |
| label: label == field.name ? null : label, |
| type: field.type, |
| namedType: field, |
| ), |
| ).toList(), |
| method: method, |
| ); |
| } |
| |
| final String name; |
| final List<_SwiftFunctionArgument> arguments; |
| final TypeDeclaration returnType; |
| final Method method; |
| } |