Skip to content

Commit

Permalink
Allow enum variant names that collide with existing types (#70)
Browse files Browse the repository at this point in the history
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 small PR tweaks the Variant classes for our typescript version of
Tagged Enums, to allow Variants to have names of existing types.

For example, it's not uncommon for Rust programs to have enums
constructed like so:

```rs
struct Dog {}
struct Cat {}

enum Animal {
  Dog(Dog),
  Cat(Cat),
}
```

The way we've modeled tagged enums are with a frozen object containing a
class for each Variant.

So this would translate to:

```typescript
type Dog = …; // Dog defined elsewhere
type Cat = …;

const Animal = (() => {
    class Dog {
        constructor(public value: Dog) {
            // This Dog value is the wrong type!
            // value is the type of the Variant class.
        }
    }
    class Cat {
        constructor(public value: Cat) {}
    }
    return Object.freeze({ Dog, Cat });
})();
```

This commit changes the naming of the Variant class:

```typescript
type Dog = …; // Dog defined elsewhere
type Cat = …;

const Animal = (() => {
    class Dog_ {
        constructor(public value: Dog) {
            // There is no variant class called Dog yet, just Dog_,
            // so value is the correct type.
        }
    }
    class Cat_ {
        constructor(public value: Cat) {}
    }
    // We rename the Dog_ variant class as Dog here,
    // so the only way to access this class is via Animal.Dog.
    return Object.freeze({ Dog: Dog_, Cat: Cat_ });
})();
```
  • Loading branch information
jhugman authored Aug 18, 2024
1 parent 1b48cc5 commit 5109542
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,23 @@ export enum {{ kind_type_name }} {
{%- call ts::docstring(e, 0) %}
export const {{ decl_type_name }} = (() => {
{%- for variant in e.variants() %}
{%- let variant_name = variant.name()|class_name(ci) %}
{#
// We have an external name and an internal name so that variants of the enum can
// have names the same as other types. Since we're building Tagged Enums from
// scratch in Typescript, we should make effort to make enums match what is possible
// or common in Rust.
// Internally to this IIFE, we use a generated name which is impossible to express
// in a different type name in Rust (we append a `_`, which would be sifted out by
// UpperCamelCasing of type names into Typescript). This lets use the class without
// colliding with the outside world. e.g. `new Variant_(record: Variant)`.
// Externally, just as we return, we name it with the Variant name, so now
// client code can use `new MyEnum.Variant(record: Variant)`.
#}
{%- let external_name = variant.name()|class_name(ci) %}
{%- let variant_name = external_name|fmt("{}_") %}
{%- let variant_data = variant_name|fmt("{}_data") %}
{%- let variant_interface = variant_name|fmt("{}_interface") %}
{%- let variant_tag = format!("{kind_type_name}.{variant_name}") %}
{%- let variant_tag = format!("{kind_type_name}.{external_name}") %}
{%- let has_fields = !variant.fields().is_empty() %}
{%- let is_tuple = variant.has_nameless_fields() %}
{%- if has_fields %}
Expand All @@ -40,7 +53,7 @@ export const {{ decl_type_name }} = (() => {
readonly inner: Readonly<{{ variant_data }}>;
{%- if !is_tuple %}
constructor(inner: { {% call ts::field_list_decl(variant, false) %} }) {
super("{{ type_name }}", "{{ variant_name }}", {{ loop.index }});
super("{{ type_name }}", "{{ external_name }}", {{ loop.index }});
this.inner = Object.freeze(inner);
}

Expand All @@ -49,7 +62,7 @@ export const {{ decl_type_name }} = (() => {
}
{%- else %}
constructor({%- call ts::field_list_decl(variant, true) -%}) {
super("{{ type_name }}", "{{ variant_name }}", {{ loop.index }});
super("{{ type_name }}", "{{ external_name }}", {{ loop.index }});
this.inner = Object.freeze([{%- call ts::field_list(variant, true) -%}]);
}

Expand All @@ -59,7 +72,7 @@ export const {{ decl_type_name }} = (() => {
{%- endif %}
{%- else %}
constructor() {
super("{{ type_name }}", "{{ variant_name }}", {{ loop.index }});
super("{{ type_name }}", "{{ external_name }}", {{ loop.index }});
}

static new(): {{ variant_name }} {
Expand Down Expand Up @@ -92,19 +105,21 @@ export const {{ decl_type_name }} = (() => {

function instanceOf(obj: any): obj is {# space #}
{%- for variant in e.variants() %}
{{- variant.name()|class_name(ci) }}
{{- variant.name()|class_name(ci)|fmt("{}_") }}
{%- if !loop.last %}| {% endif -%}
{%- endfor %} {
return obj.__uniffiTypeName === "{{ type_name }}";
}

return {
return Object.freeze({
instanceOf,
{%- for variant in e.variants() %}
{{ variant.name()|class_name(ci) }}
{%- let external_name = variant.name()|class_name(ci) %}
{%- let variant_name = external_name|fmt("{}_") %}
{{ external_name }}: {{ variant_name }}
{%- if !loop.last %}, {% endif -%}
{%- endfor %}
};
});

})();

Expand Down
14 changes: 14 additions & 0 deletions fixtures/enum-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ fn get_animal(a: Option<Animal>) -> Animal {
a.unwrap_or(Animal::Dog)
}

#[derive(uniffi::Enum)]
pub(crate) enum CollidingVariants {
AnimalRecord(AnimalRecord),
AnimalObjectInterface(Arc<AnimalObject>),
AnimalObject(Arc<AnimalObject>),
Animal(Animal),
CollidingVariants,
}

#[uniffi::export]
fn identity_colliding_variants(value: CollidingVariants) -> CollidingVariants {
value
}

uniffi::include_scaffolding!("enum_types");

#[cfg(test)]
Expand Down
73 changes: 73 additions & 0 deletions fixtures/enum-types/tests/bindings/test_enum_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import {
AnimalNamedAssociatedType_Tags,
AnimalNoReprInt,
AnimalObject,
AnimalRecord,
AnimalSignedInt,
AnimalUInt,
getAnimal,
identityEnumWithAssociatedType,
identityEnumWithNamedAssociatedType,
CollidingVariants,
CollidingVariants_Tags,
identityCollidingVariants,
} from "../../generated/enum_types";

test("Enum disriminant", (t) => {
Expand Down Expand Up @@ -100,3 +104,72 @@ test("Roundtripping enums with name values", (t) => {
assertEqual(v, identityEnumWithNamedAssociatedType(v));
}
});

test("Variant naming cam collide with existing types", (t) => {
{
const record = AnimalRecord.create({ value: 5 });
const variant1 = CollidingVariants.AnimalRecord.new(record);
const variant2 = new CollidingVariants.AnimalRecord(record);

t.assertEqual(variant1.tag, CollidingVariants_Tags.AnimalRecord);
t.assertEqual(variant2.tag, CollidingVariants_Tags.AnimalRecord);
t.assertEqual(variant1, variant2);
t.assertEqual(identityCollidingVariants(variant1), variant2);

t.assertTrue(CollidingVariants.instanceOf(variant1));
t.assertTrue(CollidingVariants.AnimalRecord.instanceOf(variant1));
}
{
const obj = new AnimalObject(1);
const variant1 = CollidingVariants.AnimalObject.new(obj);
const variant2 = new CollidingVariants.AnimalObject(obj);

t.assertEqual(variant1.tag, CollidingVariants_Tags.AnimalObject);
t.assertEqual(variant2.tag, CollidingVariants_Tags.AnimalObject);
t.assertEqual(variant1, variant2);
t.assertEqual(identityCollidingVariants(variant1), variant2);

t.assertTrue(CollidingVariants.instanceOf(variant1));
t.assertTrue(CollidingVariants.AnimalObject.instanceOf(variant1));
}

{
const obj = new AnimalObject(1);
const variant1 = CollidingVariants.AnimalObjectInterface.new(obj);
const variant2 = new CollidingVariants.AnimalObjectInterface(obj);

t.assertEqual(variant1.tag, CollidingVariants_Tags.AnimalObjectInterface);
t.assertEqual(variant2.tag, CollidingVariants_Tags.AnimalObjectInterface);
t.assertEqual(variant1, variant2);
t.assertEqual(identityCollidingVariants(variant1), variant2);

t.assertTrue(CollidingVariants.instanceOf(variant1));
t.assertTrue(CollidingVariants.AnimalObjectInterface.instanceOf(variant1));
}
{
const animal = Animal.Dog;
const variant1 = CollidingVariants.Animal.new(animal);
const variant2 = new CollidingVariants.Animal(animal);

t.assertEqual(variant1.tag, CollidingVariants_Tags.Animal);
t.assertEqual(variant2.tag, CollidingVariants_Tags.Animal);
t.assertEqual(variant1, variant2);
t.assertEqual(identityCollidingVariants(variant1), variant2);

t.assertTrue(CollidingVariants.instanceOf(variant1));
t.assertTrue(CollidingVariants.Animal.instanceOf(variant1));
}

{
const variant1 = CollidingVariants.CollidingVariants.new();
const variant2 = new CollidingVariants.CollidingVariants();

t.assertEqual(variant1.tag, CollidingVariants_Tags.CollidingVariants);
t.assertEqual(variant2.tag, CollidingVariants_Tags.CollidingVariants);
t.assertEqual(variant1, variant2);
t.assertEqual(identityCollidingVariants(variant1), variant2);

t.assertTrue(CollidingVariants.instanceOf(variant1));
t.assertTrue(CollidingVariants.CollidingVariants.instanceOf(variant1));
}
});

0 comments on commit 5109542

Please sign in to comment.