diff --git a/Cargo.lock b/Cargo.lock index 7264c4877f..1f5d55550c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1771,6 +1771,15 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-fixture-error-types-swift" +version = "0.22.0" +dependencies = [ + "anyhow", + "thiserror", + "uniffi", +] + [[package]] name = "uniffi-fixture-ext-types" version = "0.22.0" diff --git a/Cargo.toml b/Cargo.toml index a6dc4a83e4..990c20a656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "fixtures/coverall", "fixtures/callbacks", "fixtures/error-types", + "fixtures/error-types-swift", "fixtures/ext-types/custom-types", "fixtures/ext-types/http-headermap", diff --git a/docs/manual/src/swift/configuration.md b/docs/manual/src/swift/configuration.md index a7da0845bf..ca7309326e 100644 --- a/docs/manual/src/swift/configuration.md +++ b/docs/manual/src/swift/configuration.md @@ -19,6 +19,7 @@ more likely to change than other configurations. | `experimental_sendable_value_types` | `false` | Whether to mark value types as `Sendable'. | | `custom_types` | | A map which controls how custom types are exposed to Swift. See the [custom types section of the manual](../types/custom_types.md#custom-types-in-the-bindings-code) | | `omit_localized_error_conformance` | `false` | Whether to make generated error types conform to `LocalizedError`. | +| `error_enum_use_lower_camel_case` | `false` | Whether to use lower camel case for error enum variants. | [^1]: `namespace` is the top-level namespace from your UDL file. diff --git a/fixtures/error-types-swift/Cargo.toml b/fixtures/error-types-swift/Cargo.toml new file mode 100644 index 0000000000..2ac6a89aac --- /dev/null +++ b/fixtures/error-types-swift/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "uniffi-fixture-error-types-swift" +version = "0.22.0" +edition = "2021" +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "cdylib"] +name = "uniffi_error_types" + +[dependencies] +uniffi = {path = "../../uniffi"} +anyhow = "1" +thiserror = "1.0" + +[build-dependencies] +uniffi = {path = "../../uniffi", features = ["build"] } + +[dev-dependencies] +uniffi = {path = "../../uniffi", features = ["bindgen-tests"] } + +[features] +ffi-trace = ["uniffi/ffi-trace"] diff --git a/fixtures/error-types-swift/README.md b/fixtures/error-types-swift/README.md new file mode 100644 index 0000000000..65bec524d4 --- /dev/null +++ b/fixtures/error-types-swift/README.md @@ -0,0 +1 @@ +Tests for the `error_enum_use_lower_camel_case` uniffi configuration. diff --git a/fixtures/error-types-swift/build.rs b/fixtures/error-types-swift/build.rs new file mode 100644 index 0000000000..bdd47f59a1 --- /dev/null +++ b/fixtures/error-types-swift/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi::generate_scaffolding("./src/error_types.udl").unwrap(); +} diff --git a/fixtures/error-types-swift/src/error_types.udl b/fixtures/error-types-swift/src/error_types.udl new file mode 100644 index 0000000000..ad01a41f55 --- /dev/null +++ b/fixtures/error-types-swift/src/error_types.udl @@ -0,0 +1,34 @@ +namespace error_types { + [Throws=ErrorInterface] + void oops(); + + [Throws=ErrorInterface] + void oops_nowrap(); + + ErrorInterface get_error(string message); + + [Throws=RichError] + void throw_rich(string message); +}; + +interface TestInterface { + constructor(); + + [Throws=ErrorInterface, Name="fallible_new"] + constructor(); + + [Throws=ErrorInterface] + void oops(); +}; + +[Traits=(Debug, Display)] +interface ErrorInterface { + sequence chain(); + string? link(u64 index); +}; + +// Kotlin replaces a trailing "Error" with "Exception" +// for enums, so we should check it does for objects too. +[Traits=(Debug, Display)] +interface RichError { +}; diff --git a/fixtures/error-types-swift/src/lib.rs b/fixtures/error-types-swift/src/lib.rs new file mode 100644 index 0000000000..270a885806 --- /dev/null +++ b/fixtures/error-types-swift/src/lib.rs @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::Arc; + +#[derive(Debug, thiserror::Error)] +#[error("{e:?}")] +pub struct ErrorInterface { + e: anyhow::Error, +} + +impl ErrorInterface { + fn chain(&self) -> Vec { + self.e.chain().map(ToString::to_string).collect() + } + fn link(&self, ndx: u64) -> Option { + self.e.chain().nth(ndx as usize).map(ToString::to_string) + } +} + +// A conversion into our ErrorInterface from anyhow::Error. +// We can't use this implicitly yet, but it still helps. +impl From for ErrorInterface { + fn from(e: anyhow::Error) -> Self { + Self { e } + } +} + +// Test an interface as the error type +fn oops() -> Result<(), Arc> { + // must do explicit conversion to convert anyhow::Error into ErrorInterface + Err(Arc::new( + anyhow::Error::msg("oops") + .context("because uniffi told me so") + .into(), + )) +} + +// Like `oops`, but let UniFFI handle wrapping the interface with an arc +fn oops_nowrap() -> Result<(), ErrorInterface> { + // must do explicit conversion to convert anyhow::Error into ErrorInterface + Err(anyhow::Error::msg("oops") + .context("because uniffi told me so") + .into()) +} + +#[uniffi::export] +fn toops() -> Result<(), Arc> { + Err(Arc::new(ErrorTraitImpl { + m: "trait-oops".to_string(), + })) +} + +#[uniffi::export] +async fn aoops() -> Result<(), Arc> { + Err(Arc::new(anyhow::Error::msg("async-oops").into())) +} + +fn get_error(message: String) -> std::sync::Arc { + Arc::new(anyhow::Error::msg(message).into()) +} + +#[uniffi::export] +pub trait ErrorTrait: Send + Sync + std::fmt::Debug + std::error::Error { + fn msg(&self) -> String; +} + +#[derive(Debug, thiserror::Error)] +#[error("{m:?}")] +struct ErrorTraitImpl { + m: String, +} + +impl ErrorTrait for ErrorTraitImpl { + fn msg(&self) -> String { + self.m.clone() + } +} + +fn throw_rich(e: String) -> Result<(), RichError> { + Err(RichError { e }) +} + +// Exists to test trailing "Error" mapping in bindings +#[derive(Debug, thiserror::Error)] +#[error("RichError: {e:?}")] +pub struct RichError { + e: String, +} + +impl RichError {} + +pub struct TestInterface {} + +impl TestInterface { + fn new() -> Self { + TestInterface {} + } + + fn fallible_new() -> Result> { + Err(Arc::new(anyhow::Error::msg("fallible_new").into())) + } + + fn oops(&self) -> Result<(), Arc> { + Err(Arc::new( + anyhow::Error::msg("oops") + .context("because the interface told me so") + .into(), + )) + } +} + +#[uniffi::export] +impl TestInterface { + // can't define this in UDL due to #1915 + async fn aoops(&self) -> Result<(), Arc> { + Err(Arc::new(anyhow::Error::msg("async-oops").into())) + } +} + +// A procmacro as an error +#[derive(Debug, uniffi::Object, thiserror::Error)] +#[uniffi::export(Debug, Display)] +pub struct ProcErrorInterface { + e: String, +} + +#[uniffi::export] +impl ProcErrorInterface { + fn message(&self) -> String { + self.e.clone() + } +} + +impl std::fmt::Display for ProcErrorInterface { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ProcErrorInterface({})", self.e) + } +} + +#[uniffi::export] +fn throw_proc_error(e: String) -> Result<(), Arc> { + Err(Arc::new(ProcErrorInterface { e })) +} + +#[uniffi::export] +fn return_proc_error(e: String) -> Arc { + Arc::new(ProcErrorInterface { e }) +} + +#[derive(thiserror::Error, Debug)] +#[error("NonUniffiTypeValue: {v}")] +pub struct NonUniffiType { + v: String, +} + +// Note: It's important for this test that this error +// *not* be used directly as the `Err` for any functions etc. +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum Inner { + #[error("{0}")] + CaseA(String), +} + +// Note: It's important for this test that this error +// *not* be used directly as the `Err` for any functions etc. +#[derive(thiserror::Error, uniffi::Error, Debug)] +#[uniffi(flat_error)] +pub enum FlatInner { + #[error("{0}")] + CaseA(String), + #[error("{0}")] + CaseB(NonUniffiType), +} + +// Enums have good coverage elsewhere, but simple coverage here is good. +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum Error { + #[error("Oops")] + Oops, + #[error("Value: {value}")] + Value { value: String }, + #[error("IntValue: {value}")] + IntValue { value: u16 }, + #[error(transparent)] + FlatInnerError { + #[from] + error: FlatInner, + }, + #[error(transparent)] + InnerError { error: Inner }, +} + +#[uniffi::export] +fn oops_enum(i: u16) -> Result<(), Error> { + if i == 0 { + Err(Error::Oops) + } else if i == 1 { + Err(Error::Value { + value: "value".to_string(), + }) + } else if i == 2 { + Err(Error::IntValue { value: i }) + } else if i == 3 { + Err(Error::FlatInnerError { + error: FlatInner::CaseA("inner".to_string()), + }) + } else if i == 4 { + Err(Error::FlatInnerError { + error: FlatInner::CaseB(NonUniffiType { + v: "value".to_string(), + }), + }) + } else if i == 5 { + Err(Error::InnerError { + error: Inner::CaseA("inner".to_string()), + }) + } else { + panic!("unknown variant {i}") + } +} + +// tuple enum as an error. +#[derive(thiserror::Error, uniffi::Error, Debug)] +pub enum TupleError { + #[error("Oops")] + Oops(String), + #[error("Value {0}")] + Value(u16), +} + +#[uniffi::export] +fn oops_tuple(i: u16) -> Result<(), TupleError> { + if i == 0 { + Err(TupleError::Oops("oops".to_string())) + } else if i == 1 { + Err(TupleError::Value(i)) + } else { + panic!("unknown variant {i}") + } +} + +#[uniffi::export(default(t = None))] +fn get_tuple(t: Option) -> TupleError { + t.unwrap_or_else(|| TupleError::Oops("oops".to_string())) +} + +uniffi::include_scaffolding!("error_types"); diff --git a/fixtures/error-types-swift/tests/bindings/test.swift b/fixtures/error-types-swift/tests/bindings/test.swift new file mode 100644 index 0000000000..e639e4539b --- /dev/null +++ b/fixtures/error-types-swift/tests/bindings/test.swift @@ -0,0 +1,115 @@ +import error_types +do { + try oops() + fatalError("Should have thrown") +} catch let e as ErrorInterface { + let msg = "because uniffi told me so\n\nCaused by:\n oops" + assert(String(describing: e) == msg) + assert(String(reflecting: e) == "ErrorInterface { e: \(msg) }") +} + +do { + try oops() + fatalError("Should have thrown") +} catch { + let msg = "because uniffi told me so\n\nCaused by:\n oops" + assert(String(describing: error) == msg) + assert(String(reflecting: error) == "ErrorInterface { e: \(msg) }") + assert(error.localizedDescription == "ErrorInterface { e: \(msg) }") +} + +do { + try oopsEnum(i: 0) + fatalError("Should have thrown") +} catch let e as Error { + assert(e == Error.oops) + assert(String(describing: e) == "oops") + assert(String(reflecting: e) == "error_types.Error.oops") +} +do { + try oopsEnum(i: 0) + fatalError("Should have thrown") +} catch { + assert(String(describing: error) == "oops") + assert(String(reflecting: error) == "error_types.Error.oops") + assert(error.localizedDescription == "error_types.Error.oops") +} + +do { + try oopsEnum(i: 1) + fatalError("Should have thrown") +} catch { + assert(String(describing: error) == "value(value: \"value\")") + assert(String(reflecting: error) == "error_types.Error.value(value: \"value\")") + assert(error.localizedDescription == "error_types.Error.value(value: \"value\")") +} + +do { + try oopsEnum(i: 2) + fatalError("Should have thrown") +} catch { + assert(String(describing: error) == "intValue(value: 2)") + assert(String(reflecting: error) == "error_types.Error.intValue(value: 2)") + assert(error.localizedDescription == "error_types.Error.intValue(value: 2)") +} + +do { + try oopsEnum(i: 3) + fatalError("Should have thrown") +} catch let e as Error { + assert(String(describing: e) == "flatInnerError(error: error_types.FlatInner.caseA(message: \"inner\"))") + assert(String(reflecting: e) == "error_types.Error.flatInnerError(error: error_types.FlatInner.caseA(message: \"inner\"))") +} + +do { + try oopsEnum(i: 4) + fatalError("Should have thrown") +} catch let e as Error { + assert(String(describing: e) == "flatInnerError(error: error_types.FlatInner.caseB(message: \"NonUniffiTypeValue: value\"))") + assert(String(reflecting: e) == "error_types.Error.flatInnerError(error: error_types.FlatInner.caseB(message: \"NonUniffiTypeValue: value\"))") +} + +do { + try oopsEnum(i: 5) + fatalError("Should have thrown") +} catch let e as Error { + assert(String(describing: e) == "innerError(error: error_types.Inner.caseA(\"inner\"))") +} + +do { + try oopsTuple(i: 0) + fatalError("Should have thrown") +} catch { + assert(String(describing: error) == "oops(\"oops\")") + assert(String(reflecting: error) == "error_types.TupleError.oops(\"oops\")") + assert(error.localizedDescription == "error_types.TupleError.oops(\"oops\")") +} + +do { + try oopsTuple(i: 1) + fatalError("Should have thrown") +} catch { + assert(String(describing: error) == "value(1)") + assert(String(reflecting: error) == "error_types.TupleError.value(1)") + assert(error.localizedDescription == "error_types.TupleError.value(1)") +} + +do { + try oopsNowrap() + fatalError("Should have thrown") +} catch let e as ErrorInterface { + assert(String(describing: e) == "because uniffi told me so\n\nCaused by:\n oops") +} + +do { + try toops() + fatalError("Should have thrown") +} catch let e as ErrorTrait { + assert(e.msg() == "trait-oops") +} + +let e = getError(message: "the error") +assert(String(describing: e) == "the error") +assert(String(reflecting: e) == "ErrorInterface { e: the error }") +// assert(Error.self is Swift.Error.Type) -- always true! +assert(Error.self != Swift.Error.self) diff --git a/fixtures/error-types-swift/tests/test_generated_bindings.rs b/fixtures/error-types-swift/tests/test_generated_bindings.rs new file mode 100644 index 0000000000..62aea4ddb2 --- /dev/null +++ b/fixtures/error-types-swift/tests/test_generated_bindings.rs @@ -0,0 +1,3 @@ +uniffi::build_foreign_language_testcases!( + "tests/bindings/test.swift" +); diff --git a/fixtures/error-types-swift/uniffi.toml b/fixtures/error-types-swift/uniffi.toml new file mode 100644 index 0000000000..8985865eee --- /dev/null +++ b/fixtures/error-types-swift/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.swift] +error_enum_use_lower_camel_case = true diff --git a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs index 3b5f4404a8..584ea6cb12 100644 --- a/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs +++ b/uniffi_bindgen/src/bindings/swift/gen_swift/mod.rs @@ -198,6 +198,7 @@ pub struct Config { generate_immutable_records: Option, experimental_sendable_value_types: Option, omit_localized_error_conformance: Option, + error_enum_use_lower_camel_case: Option, #[serde(default)] custom_types: HashMap, } @@ -270,6 +271,11 @@ impl Config { pub fn omit_localized_error_conformance(&self) -> bool { self.omit_localized_error_conformance.unwrap_or(false) } + + /// Whether to use lower camel case for error enum variants. Default: false. + pub fn error_enum_use_lower_camel_case(&self) -> bool { + self.error_enum_use_lower_camel_case.unwrap_or(false) + } } /// Generate UniFFI component bindings for Swift, as strings in memory. @@ -754,9 +760,13 @@ pub mod filters { Ok(quote_general_keyword(oracle().enum_variant_name(nm))) } - /// Like enum_variant_swift_quoted, but a class name. - pub fn error_variant_swift_quoted(nm: &str) -> Result { - Ok(quote_general_keyword(oracle().class_name(nm))) + /// Get the Swift rendering of an individual enum variant for an Error type. + pub fn error_variant_swift_quoted(nm: &str, use_class_name: &bool) -> Result { + if *use_class_name { + Ok(quote_general_keyword(oracle().class_name(nm))) + } else { + Ok(quote_general_keyword(oracle().enum_variant_name(nm))) + } } /// Get the idiomatic Swift rendering of an FFI callback function name diff --git a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift index 3be1d562b7..7ba8c3b63f 100644 --- a/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift +++ b/uniffi_bindgen/src/bindings/swift/templates/ErrorTemplate.swift @@ -1,16 +1,17 @@ {%- call swift::docstring(e, 0) %} +{% let use_class_name = !config.error_enum_use_lower_camel_case() %} public enum {{ type_name }} { {% if e.is_flat() %} {% for variant in e.variants() %} {%- call swift::docstring(variant, 4) %} - case {{ variant.name()|class_name }}(message: String) + case {{ variant.name()|error_variant_swift_quoted(use_class_name) }}(message: String) {% endfor %} {%- else %} {% for variant in e.variants() %} {%- call swift::docstring(variant, 4) %} - case {{ variant.name()|class_name }}{% if variant.fields().len() > 0 %}( + case {{ variant.name()|error_variant_swift_quoted(use_class_name) }}{% if variant.fields().len() > 0 %}( {%- call swift::field_list_decl(variant, variant.has_nameless_fields()) %} ){% endif -%} {% endfor %} @@ -32,7 +33,7 @@ public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { {% if e.is_flat() %} {% for variant in e.variants() %} - case {{ loop.index }}: return .{{ variant.name()|class_name }}( + case {{ loop.index }}: return .{{ variant.name()|error_variant_swift_quoted(use_class_name) }}( message: try {{ Type::String.borrow()|read_fn }}(from: &buf) ) {% endfor %} @@ -40,7 +41,7 @@ public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { {% else %} {% for variant in e.variants() %} - case {{ loop.index }}: return .{{ variant.name()|error_variant_swift_quoted }}{% if variant.has_fields() %}( + case {{ loop.index }}: return .{{ variant.name()|error_variant_swift_quoted(use_class_name) }}{% if variant.has_fields() %}( {% for field in variant.fields() -%} {%- if variant.has_nameless_fields() -%} try {{ field|read_fn }}(from: &buf) @@ -63,7 +64,7 @@ public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { {% if e.is_flat() %} {% for variant in e.variants() %} - case .{{ variant.name()|class_name }}(_ /* message is ignored*/): + case .{{ variant.name()|error_variant_swift_quoted(use_class_name) }}(_ /* message is ignored*/): writeInt(&buf, Int32({{ loop.index }})) {%- endfor %} @@ -71,13 +72,13 @@ public struct {{ ffi_converter_name }}: FfiConverterRustBuffer { {% for variant in e.variants() %} {% if variant.has_fields() %} - case let .{{ variant.name()|error_variant_swift_quoted }}({% for field in variant.fields() %}{%- call swift::field_name(field, loop.index) -%}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): + case let .{{ variant.name()|error_variant_swift_quoted(use_class_name) }}({% for field in variant.fields() %}{%- call swift::field_name(field, loop.index) -%}{%- if loop.last -%}{%- else -%},{%- endif -%}{% endfor %}): writeInt(&buf, Int32({{ loop.index }})) {% for field in variant.fields() -%} {{ field|write_fn }}({% call swift::field_name(field, loop.index) %}, into: &buf) {% endfor -%} {% else %} - case .{{ variant.name()|class_name }}: + case .{{ variant.name()|error_variant_swift_quoted(use_class_name) }}: writeInt(&buf, Int32({{ loop.index }})) {% endif %} {%- endfor %}