Skip to content

Commit

Permalink
Add tag property to flat errors (#117)
Browse files Browse the repository at this point in the history
Fixes #111.

According to [The Big O of Code
Reviews](https://www.egorand.dev/the-big-o-of-code-reviews/), this is a
O(_n_) change.

This PR adds a `tag` property and a `MyError_Tags` enum to flat errors.

to the error template, it adds some testing of the template, and some
docs.
  • Loading branch information
jhugman authored Oct 9, 2024
1 parent 3891cab commit 4cda9a3
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
{{- self.import_infra("UniffiError", "errors") }}
{{- self.import_infra("uniffiTypeNameSymbol", "symbols") }}
{{- self.import_infra("variantOrdinalSymbol", "symbols") }}
{%- let instance_of = "instanceOf" %}
{%- let flat = e.is_flat() %}
{%- if flat %}

// Flat error type: {{ decl_type_name }}
{%- let type_name__Tags = format!("{type_name}_Tags") %}
export enum {{ type_name__Tags }} {
{%- for variant in e.variants() %}
{{ variant|variant_name }} = "{{ variant.name() }}"
{%- if !loop.last %},{% endif -%}
{% endfor %}
}

{%- call ts::docstring(e, 0) %}
export const {{ decl_type_name }} = (() => {
{%- for variant in e.variants() %}
Expand All @@ -21,31 +29,30 @@ export const {{ decl_type_name }} = (() => {
* This field is private and should not be used.
*/
readonly [variantOrdinalSymbol] = {{ loop.index }};

public readonly tag = {{ type_name__Tags }}.{{ variant|variant_name }};

constructor(message: string) {
super("{{ type_name }}", "{{ variant_name }}", message);
}

static {{ instance_of }}(e: any): e is {{ variant_name }} {
static instanceOf(e: any): e is {{ variant_name }} {
return (
{{ instance_of }}(e) && (e as any)[variantOrdinalSymbol] === {{ loop.index }}
instanceOf(e) && (e as any)[variantOrdinalSymbol] === {{ loop.index }}
);
}
}
{%- endfor %}

// Utility function which does not rely on instanceof.
function {{ instance_of }}(e: any): e is {# space #}
{%- for variant in e.variants() %}
{{- variant.name()|class_name(ci) }}
{%- if !loop.last %} | {% endif -%}
{%- endfor %} {
function instanceOf(e: any): e is {{ type_name }} {
return (e as any)[uniffiTypeNameSymbol] === "{{ decl_type_name }}";
}
return {
{%- for variant in e.variants() %}
{{ variant.name()|class_name(ci) }},
{%- endfor %}
{{ instance_of }},
instanceOf,
};
})();

Expand Down
83 changes: 82 additions & 1 deletion docs/src/idioms/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ In Rust, instead of throwing with try/catch, a method returns a `Result` enum.
#[derive(uniffi::Error)]
pub enum MathError {
DivideByZero,
NumberOverflow,
}

#[uniffi::export]
Expand Down Expand Up @@ -63,7 +64,32 @@ try {
}
```

Enums-as-errors may also have properties. These are exactly the same as [other enums](./enums.md#enums-with-properties), except they subclass `Error`.
Such enums as errors, without properties also have a companion `_Tags` enum.

Using a `switch` on the error's `tag` property is a convenient way of handling all cases:

```typescript
try {
divide(x, y);
} catch (e: any) {
if (MathError.instanceOf(e)) {
switch (e.tag) {
case MathError_Tags.DivideByZero: {
// handle divide by zero
break;
}
case MathError_Tahs.NumberOverflow: {
// handle overflow
break;
}
}
}
}
```

#### Enums with properties as Errors

Enums-as-errors may also have properties. These are exactly the same as [other enums with properties](./enums.md#enums-with-properties), except they subclass `Error`.

e.g.

Expand Down Expand Up @@ -108,6 +134,61 @@ try {

```
#### Flat errors
A common pattern in Rust is to convert enum properties to a message. Uniffi calls these error enums `flat_errors`.
In this example, a `MyError::InvalidDataError` has no properties but gets the message `"Invalid data"`, `ParseError` converts its properties in to a message, and `JSONError` takes any `serde_json::Error` to make a `JSONError`, which then gets converted to a string.
In this case, the conversion is being managed by the `thiserror` crate's macros.
```rust
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum MyError {
// A message from a variant with no properties
#[error("Invalid data")]
InvalidDataError,

// A message from a variant with named properties
#[error("Parse error at line {line}, column {col}")]
ParseError { line: usize, col: usize },

// A message from an JSON error, converted into a MyError
#[error("JSON Error: {0}")]
JSONError(#[from] serde_json::Error),
}
```
Unlike [flat enums](enums.md#enums-without-properties), flat errors have a `tag` property and a companion `MyError_Tags` enum.
These can be handled in typescript like so:
```typescript
try {
// … do sometihng that throws
} catch (err: any) {
if (MyError.instanceOf(err)) {
switch (err.tag) {
case MyError_Tags.InvalidDataError: {
// e.message will be "MyError.InvalidDataError: Invalid data"
break;
}
case MyError_Tags.ParseError: {
// e.message will be paramterized, e.g.
// "MyError.ParseError: Parse error at line 12, column 4"
break;
}
case MyError_Tags.JSONError: {
// e.message will be a wrapped serde_json error, e.g.
// "MyError.JSONError: Expected , \" or \]"
break;
}
}
}
}
```
### Objects as Errors
As you may have gathered, in Rust errors can be anything including objects. In the rare occasions this may be useful:
Expand Down
50 changes: 35 additions & 15 deletions fixtures/error-types/tests/bindings/test_error_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
ErrorInterface,
ErrorTrait,
Exception,
Exception_Tags,
FlatInner,
FlatInner_Tags,
getError,
Inner,
oops,
Expand Down Expand Up @@ -97,16 +99,27 @@ test("oopsEnum 2", (t) => {
test("oopsEnum 3", (t) => {
t.assertThrows(
(error) => {
if (Exception.FlatInnerError.instanceOf(error)) {
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
t.assertTrue(FlatInner.CaseA.instanceOf(error.inner.error));
t.assertEqual(
error.inner.error.toString(),
"Error: FlatInner.CaseA: inner",
);
// 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\"))")
return true;
if (Exception.instanceOf(error)) {
switch (error.tag) {
case Exception_Tags.FlatInnerError: {
t.assertTrue(Exception.FlatInnerError.instanceOf(error));
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
const inner = error.inner.error;
t.assertEqual(inner.toString(), "Error: FlatInner.CaseA: inner");
switch (inner.tag) {
case FlatInner_Tags.CaseA: {
t.assertTrue(FlatInner.CaseA.instanceOf(inner));
t.assertFalse(FlatInner.CaseB.instanceOf(inner));
return true;
}
default: {
// NOOP
}
}
}
default:
// NOOP
}
}
return false;
},
Expand All @@ -120,15 +133,22 @@ test("oopsEnum 4", (t) => {
if (Exception.FlatInnerError.instanceOf(error)) {
t.assertEqual(error.toString(), "Error: Exception.FlatInnerError");
if (Exception.FlatInnerError.hasInner(error)) {
const inner = Exception.FlatInnerError.getInner(error);
if (FlatInner.CaseB.instanceOf(inner)) {
t.assertEqual(inner.error.toString(), "NonUniffiTypeValue: value");
return true;
const innerError = Exception.FlatInnerError.getInner(error);
const inner = innerError.error;
if (FlatInner.instanceOf(inner)) {
switch (inner.tag) {
case FlatInner_Tags.CaseA:
t.assertTrue(FlatInner.CaseA.instanceOf(inner));
return true;
case FlatInner_Tags.CaseB:
// We know that the Rust only produces this variant.
t.assertTrue(FlatInner.CaseB.instanceOf(inner));
return true;
}
}
}
// 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\"))")
return true;
}
return false;
},
Expand Down

0 comments on commit 4cda9a3

Please sign in to comment.