-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[pigeon][swift] Removes FlutterError in favor of PigeonError #6611
Changes from 2 commits
c510bd6
d8968d7
c2beea1
6a8a94c
b874f09
df102e3
b30a0cc
30ac050
92c97af
86d9168
20ac87a
800ba60
8fa6b75
4bd3dbc
fae49ea
b386a96
78c3bb6
f13a06a
63df915
428736d
88cd27d
a78cae7
4ed7852
a8ab4f8
877015d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,32 @@ import Foundation | |
#error("Unsupported platform.") | ||
#endif | ||
|
||
/// Error thrown by Pigeon. Encapsulates a code, message, and details. | ||
class PigeonError: Swift.Error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
let code: String | ||
let message: String? | ||
let details: Any? | ||
|
||
init(code: String, message: String?, details: Any?) { | ||
self.code = code | ||
self.message = message | ||
self.details = details | ||
} | ||
|
||
var localizedDescription: String { | ||
let detailsDescription: String | ||
if let convertibleObject = details as? CustomStringConvertible { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does string interpolation already cover the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. I thought I had to check multiple cases, but aparently the simple solution is enough. Done in c2beea1. |
||
detailsDescription = convertibleObject.description | ||
} else if let _ = details { | ||
detailsDescription = "<non-convertible object>" | ||
} else { | ||
detailsDescription = "<nil>" | ||
} | ||
return | ||
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(detailsDescription)" | ||
} | ||
} | ||
|
||
private func wrapResult(_ result: Any?) -> [Any?] { | ||
return [result] | ||
} | ||
|
@@ -33,8 +59,8 @@ private func wrapError(_ error: Any) -> [Any?] { | |
] | ||
} | ||
|
||
private func createConnectionError(withChannelName channelName: String) -> FlutterError { | ||
return FlutterError( | ||
private func createConnectionError(withChannelName channelName: String) -> PigeonError { | ||
return PigeonError( | ||
code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", | ||
details: "") | ||
} | ||
|
@@ -193,7 +219,7 @@ class ExampleHostApiSetup { | |
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. | ||
protocol MessageFlutterApiProtocol { | ||
func flutterMethod( | ||
aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void) | ||
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void) | ||
} | ||
class MessageFlutterApi: MessageFlutterApiProtocol { | ||
private let binaryMessenger: FlutterBinaryMessenger | ||
|
@@ -203,7 +229,7 @@ class MessageFlutterApi: MessageFlutterApiProtocol { | |
self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" | ||
} | ||
func flutterMethod( | ||
aString aStringArg: String?, completion: @escaping (Result<String, FlutterError>) -> Void | ||
aString aStringArg: String?, completion: @escaping (Result<String, PigeonError>) -> Void | ||
) { | ||
let channelName: String = | ||
"dev.flutter.pigeon.pigeon_example_package.MessageFlutterApi.flutterMethod\(messageChannelSuffix)" | ||
|
@@ -217,11 +243,11 @@ class MessageFlutterApi: MessageFlutterApiProtocol { | |
let code: String = listResponse[0] as! String | ||
let message: String? = nilOrValue(listResponse[1]) | ||
let details: String? = nilOrValue(listResponse[2]) | ||
completion(.failure(FlutterError(code: code, message: message, details: details))) | ||
completion(.failure(PigeonError(code: code, message: message, details: details))) | ||
} else if listResponse[0] == nil { | ||
completion( | ||
.failure( | ||
FlutterError( | ||
PigeonError( | ||
code: "null-error", | ||
message: "Flutter api returned null value for non-null return value.", details: ""))) | ||
} else { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,16 +19,22 @@ class SwiftOptions { | |
/// Creates a [SwiftOptions] object | ||
const SwiftOptions({ | ||
this.copyrightHeader, | ||
this.swiftEmitErrorClass, | ||
}); | ||
|
||
/// A copyright header that will get prepended to generated code. | ||
final Iterable<String>? copyrightHeader; | ||
|
||
/// Whether to emit the PigeonError class in the generated code. | ||
/// Defaults to true. | ||
final bool? swiftEmitErrorClass; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// 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>?, | ||
swiftEmitErrorClass: map['swiftEmitErrorClass'] as bool?, | ||
); | ||
} | ||
|
||
|
@@ -37,6 +43,8 @@ class SwiftOptions { | |
Map<String, Object> toMap() { | ||
final Map<String, Object> result = <String, Object>{ | ||
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!, | ||
if (swiftEmitErrorClass != null) | ||
'swiftEmitErrorClass': swiftEmitErrorClass!, | ||
}; | ||
return result; | ||
} | ||
|
@@ -316,7 +324,7 @@ class SwiftGenerator extends StructuredGenerator<SwiftOptions> { | |
name: func.name, | ||
parameters: func.parameters, | ||
returnType: func.returnType, | ||
errorTypeName: 'FlutterError', | ||
errorTypeName: 'PigeonError', | ||
isAsynchronous: true, | ||
swiftFunction: func.swiftFunction, | ||
getParameterName: _getSafeArgumentName, | ||
|
@@ -673,10 +681,10 @@ private func nilOrValue<T>(_ value: Any?) -> T? { | |
void _writeCreateConnectionError(Indent indent) { | ||
indent.newln(); | ||
indent.writeScoped( | ||
'private func createConnectionError(withChannelName channelName: String) -> FlutterError {', | ||
'private func createConnectionError(withChannelName channelName: String) -> PigeonError {', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens to existing code that throws There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone have a code that throws There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah that will be a breaking change and may cause chaos. We should still make sure the old approach works. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you provide the names of open-source downstream projects that I can check to see if this PR breaks them? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Compile time breaking changes in Pigeon are fine; we do them pretty frequently. Clients of Pigeon are fully in control of when they update their Pigeon generation, and Pigeon isn't a transitive dependency in package trees so there aren't any version conflict concerns. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh i was referring to runtime breaking changes. let's say we have this old code:
Then when the customer bump the version, which doesn't handle the
Since no compile error, developers may not update their code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I'll explain this using the source code of test codes in the pigeon package. // https://github.com/flutter/packages/blob/dd01140f470e02b06d05ffa0213c7b26e89ae4c4/packages/pigeon/pigeons/core_tests.dart#L152
@HostApi()
abstract class HostIntegrationCoreApi {
...
/// Returns an error, to test error handling.
Object? throwError();
} will be generated like this: // https://github.com/flutter/packages/blob/dd01140f470e02b06d05ffa0213c7b26e89ae4c4/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L466
protocol HostIntegrationCoreApi {
...
/// Returns an error, to test error handling.
func throwError() throws -> Any?
}
...
// https://github.com/flutter/packages/blob/dd01140f470e02b06d05ffa0213c7b26e89ae4c4/packages/pigeon/platform_tests/test_plugin/ios/Classes/CoreTests.gen.swift#L701
let throwErrorChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.pigeon_integration_tests.HostIntegrationCoreApi.throwError\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
throwErrorChannel.setMessageHandler { _, reply in
do {
let result = try api.throwError()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
throwErrorChannel.setMessageHandler(nil)
}
... and current implementation of // https://github.com/flutter/packages/blob/dd01140f470e02b06d05ffa0213c7b26e89ae4c4/packages/pigeon/platform_tests/test_plugin/ios/Classes/TestPlugin.swift#L52
func throwError() throws -> Any? {
throw FlutterError(code: "code", message: "message", details: "details")
} Also, current implementation of private func wrapError(_ error: Any) -> [Any?] {
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
} I believe the goal of Even we convert errors in the generated swift code to use This PR's private func wrapError(_ error: Any) -> [Any?] {
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
} Then, changes in the generated code won't make any problem in the dart side, even some older Swift code is throwing In short, I believe that no compatibility issue will occur even after this PR is merged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, right. I was thinking of the aync functions, where the type is part of the signature. We definitely still need |
||
'}', () { | ||
indent.writeln( | ||
'return FlutterError(code: "channel-error", message: "Unable to establish connection on channel: \'\\(channelName)\'.", details: "")'); | ||
'return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: \'\\(channelName)\'.", details: "")'); | ||
}); | ||
} | ||
|
||
|
@@ -694,6 +702,10 @@ private func nilOrValue<T>(_ value: Any?) -> T? { | |
.whereType<AstFlutterApi>() | ||
.any((Api api) => api.methods.isNotEmpty); | ||
|
||
if (generatorOptions.swiftEmitErrorClass ?? true) { | ||
_writePigeonError(indent); | ||
} | ||
|
||
if (hasHostApi) { | ||
_writeWrapResult(indent); | ||
_writeWrapError(indent); | ||
|
@@ -718,7 +730,7 @@ private func nilOrValue<T>(_ value: Any?) -> T? { | |
name: name, | ||
parameters: parameters, | ||
returnType: returnType, | ||
errorTypeName: 'FlutterError', | ||
errorTypeName: 'PigeonError', | ||
isAsynchronous: true, | ||
swiftFunction: swiftFunction, | ||
getParameterName: _getSafeArgumentName, | ||
|
@@ -760,12 +772,12 @@ private func nilOrValue<T>(_ value: Any?) -> T? { | |
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)))'); | ||
'completion(.failure(PigeonError(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: "")))'); | ||
'completion(.failure(PigeonError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))'); | ||
}, addTrailingNewline: false); | ||
} | ||
indent.addScoped('else {', '}', () { | ||
|
@@ -895,6 +907,51 @@ private func nilOrValue<T>(_ value: Any?) -> T? { | |
indent.writeln('$varChannelName.setMessageHandler(nil)'); | ||
}); | ||
} | ||
|
||
void _writePigeonError(Indent indent) { | ||
indent.newln(); | ||
indent.writeln( | ||
'/// Error thrown by Pigeon. Encapsulates a code, message, and details.'); | ||
indent.writeln('class PigeonError: Swift.Error {'); | ||
indent.nest(1, () { | ||
tarrinneal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
indent.writeln('let code: String'); | ||
indent.writeln('let message: String?'); | ||
indent.writeln('let details: Any?'); | ||
indent.newln(); | ||
indent.writeln('init(code: String, message: String?, details: Any?) {'); | ||
indent.nest(1, () { | ||
indent.writeln('self.code = code'); | ||
indent.writeln('self.message = message'); | ||
indent.writeln('self.details = details'); | ||
}); | ||
indent.writeln('}'); | ||
indent.newln(); | ||
indent.writeln('var localizedDescription: String {'); | ||
indent.nest(1, () { | ||
indent.writeln('let detailsDescription: String'); | ||
indent.writeln( | ||
'if let convertibleObject = details as? CustomStringConvertible {'); | ||
indent.nest(1, () { | ||
indent.writeln('detailsDescription = convertibleObject.description'); | ||
}); | ||
indent.writeln('} else if let _ = details {'); | ||
indent.nest(1, () { | ||
indent.writeln('detailsDescription = "<non-convertible object>"'); | ||
}); | ||
indent.writeln('} else {'); | ||
indent.nest(1, () { | ||
indent.writeln('detailsDescription = "<nil>"'); | ||
}); | ||
indent.writeln('}'); | ||
indent.writeln( | ||
r'return "PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(detailsDescription)"'); | ||
}); | ||
indent.write('}'); | ||
}); | ||
indent.newln(); | ||
indent.write('}'); | ||
indent.newln(); | ||
} | ||
} | ||
|
||
/// Calculates the name of the codec that will be generated for [api]. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add an example here to replace existing example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.