tagset

Crates.iotagset
lib.rstagset
version0.1.1
created_at2025-07-10 05:44:20.62279+00
updated_at2025-07-17 23:58:27.915394+00
descriptionEasily create trait-dispatching sum types
homepage
repositoryhttps://github.com/colinjneville/tagset
max_upload_size
id1745914
size52,977
(colinjneville)

documentation

README

Easily create trait-dispatching sum types (TagSets).

  • Fixed discriminant values - Each included type's discriminant will not change, even as new variants are added in future versions.
  • Build-in serde support - Easily derive Serialize and Deserialize for your set.
  • Supports any trait - Trait items which are not dispatchable (such as associated types) can be explicitly provided.
  • Compositable sets - Sets can easily include other sets as sub-sets.
  • Extended trait defaults - Annotate your trait to allow for trait defaults not supported by standard Rust, such as associated type defaults.
  • (Mostly) scope-independent - With a few rare exceptions, nothing is required to be in scope, and types and traits are not required to have unique identifiers.
/// Mark traits with `telety`(<https://crates.io/crates/telety>):
pub mod my_trait {
    #[telety::telety(crate::my_trait)]
    pub trait MyTrait {
        fn ascii_char(&self) -> u8;
    }
}
pub mod my_impl {
    #[derive(Debug)]
    pub struct A;
    impl super::my_trait::MyTrait for A {
        fn ascii_char(&self) -> u8 {
            b'A'
        }
    }
    #[derive(Debug)]
    pub struct B;
    impl super::my_trait::MyTrait for B {
        fn ascii_char(&self) -> u8 {
            b'B'
        }
    }
}
pub mod my_set {
    # use tagset::tagset;
    // Define your dispatching set type using tagset attributes:
    // Implements `MyTrait` for `MyImplementor`
    #[tagset(impl super::my_trait::MyTrait)]
    // Adds `A` as a variant of `MyImplementer` and generates a `From<A>` and `TryFrom<MySet>` impl
    #[tagset(super::my_impl::A)]
    // Adds `B` as a variant of `MyImplementer` and generates a `From<B>` and `TryFrom<MySet>` impl
    #[tagset(super::my_impl::B)]
    #[tagset(derive(Debug))]
    pub struct MySet;
}
fn main() {
    use my_trait::MyTrait as _;
    // MySet implements From for all variants
    let set_a: my_set::MySet = my_impl::A.into();
    let set_b: my_set::MySet = my_impl::B.into();
    assert_eq!(set_a.ascii_char(), b'A');
    assert_eq!(set_b.ascii_char(), b'B');

    // TryFrom<MySet> is implemented for all variants,
    // as well as &MySet and &mut MySet versions
    let a: my_impl::A = set_a.try_into().unwrap();
    // Conversion by value returns the original value in Err
    let result_b: Result<my_impl::A, my_set::MySet> = set_b.try_into();
    let set_b: my_set::MySet = result_b.unwrap_err();
}

You can also add generic parameters and bounds:

pub mod my_trait {
    #[telety::telety(crate::my_trait)]
    pub trait MyTrait<U> { }
}
pub mod my_impl {
    #[derive(Clone)]
    pub struct Parameter<T>(T);
    impl<T, U> super::my_trait::MyTrait<U> for Parameter<T> { }
}
pub mod my_set {
    # use tagset::tagset;
    #[tagset(impl<U> super::my_trait::MyTrait<U>
        // See the type bounds section for details
        where for<VAR> VAR: Clone
    )]
    // You can use type parameters from the set
    #[tagset(super::my_impl::Parameter<T>)]
    pub struct MySet<T>;
}
# fn main() { }

Discriminants

tagset supports consistent discriminants. Like enums, by default numbering is incremental starting at 0.
To choose the initial discriminant, use the #[tagset(index(...))] attribute. index takes an integer literal with an optional suffix if you want to specify the discriminant type. e.g., [tagset(index(-4ii8))] sets the discriminant type as i8, and the first discriminant value to -4.
To modify the default numbering, use #[tagset(reserved(...))] or #[tagset(deprecated(...))] attributes. (Both are functually equivalent, but semantically distinguish discriminants that can and cannot be used in the future). Both attributes take a range literal with an open start and an inclusive or exclusive end, e.g. ..5 or ..=5. All discriminants starting after the previous variant to the end of the range will be skipped.

# use tagset::tagset;
pub struct A;
pub struct B;
pub struct C;
#[tagset(index(2u8))]
#[tagset(A)] // 2
#[tagset(deprecated(..5))]
#[tagset(B)] // 5
#[tagset(C)] // 6
#[tagset(reserved(..16))]
pub struct MySet;
fn main() {
    use tagset::TagSet as _;
    let a: MySet = A.into();
    let b: MySet = B.into();
    let c: MySet = C.into();
    
    assert_eq!(a.discriminant(), 2u8);
    assert_eq!(b.discriminant(), 5u8);
    assert_eq!(c.discriminant(), 6u8);
}

Discriminant values are available at runtime through the [TagSet] trait, and at compile-time with [TagSetDiscriminant] (variant type to variant discriminant) and [TagSetTypeI32] (variant discriminant to variant ) (use the trait corresponding to the discriminant type, [TagSetTypeU8], etc.).

External traits

If a trait comes from an external crate, you can create a telety proxy for it:

pub mod external_crate {
    // No telety attribute
    pub trait MyTrait {
        fn ascii_char(&self) -> u8;
    }
}
pub mod internal_proxy {
    // The actual trait from external_crate is re-exported here, along with the
    // telety info as if it were defined here
    #[telety::telety(crate::internal_proxy, proxy = "crate::external_crate::MyTrait")]
    pub trait MyTrait {
        fn ascii_char(&self) -> u8;
    }
}
pub mod proxy_impl {
    # use tagset::tagset;
    pub struct A;
    
    impl super::external_crate::MyTrait for A {
        fn ascii_char(&self) -> u8 {
            b'A'
        }
    }
    pub struct B;
    // Either external_crate::MyTrait or internal_proxy::MyTrait can be used -
    // they refer to the exact same trait
    impl super::internal_proxy::MyTrait for B {
        fn ascii_char(&self) -> u8 {
            b'B'
        }
    }
    #[tagset(impl super::internal_proxy::MyTrait)]
    #[tagset(A)]
    #[tagset(B)]
    pub struct Set;
}
# fn main() { }

Overrides

You can override item definitions inside traits:

pub mod my_trait {
    #[telety::telety(crate::my_trait)]
    pub trait MyTrait {
        type Output;
        fn get(&self) -> Self::Output;
    }
}
pub mod my_impl {
    pub struct A(pub i32);
    
    impl super::my_trait::MyTrait for A {
        type Output = i32;
        fn get(&self) -> Self::Output {
            self.0
        }
    }
    pub struct B(pub u32);
    
    impl super::my_trait::MyTrait for B {
        type Output = u32;
        fn get(&self) -> Self::Output {
            self.0
        }
    }
}
pub mod my_set {
    # use tagset::tagset;
    #[tagset(impl super::my_trait::MyTrait {
        // associated types usually require an override
        type Output = i64;
        // trait functions will usually delegate to the variant by default,
        // but sometimes this is not possible (e.g. there is no reciever,
        // or the reciever is not convertable such as Rc<Self>), or not
        // desired.
        // Overrides are provided 2 macros to generate implementations based
        // based on the included types:
        //   `match_by_value!(value, value_identifier => expr)`
        //     `value` is a value of type `Self`, `&Self`, or `&mut Self`.
        //     `value_identifier` is any identifier to be used in `expr` as
        //       the inner value of the set.
        //     `expr` is an expression in which `value_identifier` is the
        //       inner value of the set.
        //   `match_by_discriminant!(discriminant, type_identifier => expr)`
        //     `discriminant` is a value of the type of the set's discriminant.
        //       If the value is invalid, the function will panic.
        //     `type_identifier` is any identifier to be substituted with the
        //       type path corresponding to `discriminant`.
        //     `expr` is an expression in which `type_identifier` will be
        //       substituted for `discriminant`'s corresponding type path.
        fn get(&self) -> Self::Output {
            match_by_value!(self, value => value.get() as i64)
        }
    })]
    #[tagset(super::my_impl::A)]
    #[tagset(super::my_impl::B)]
    pub struct MySet;
}
fn main() {
    use my_trait::MyTrait as _;
    let a: my_set::MySet = my_impl::A(3i32).into();
    let b: my_set::MySet = my_impl::B(4u32).into();
    
    assert_eq!(a.get() + b.get(), 7);
}

Type bounds

tagset support a special syntax when specifying type bounds:
for <VAR>: VAR: MyTrait
This bound is expanded for variant, substituting VAR in the bound for the type path of the variant. For example, in a set containing types A, B, and C, this is equivalent to:

    A: MyTrait,
    B: MyTrait,
    C: MyTrait,

Includes

Sets are compositable: you can use #[tagset(include(MySubSet))] to include all variant types from a telety-enabled MySubSet.
Included types will retain their relative discriminants, but not their absolute discriminants.

pub mod my_subset {
    # use tagset::tagset;
    pub struct A;
    pub struct B;
    
    #[telety::telety(crate::my_subset)]
    #[tagset(index(7i32))]
    #[tagset(A)]
    #[tagset(reserved(..11))]
    #[tagset(B)]
    pub struct MySubSet;
}
pub mod my_set {
    # use tagset::tagset;
    pub struct C;
    pub struct D;
    #[tagset(index(3u64))]
    #[tagset(C)]
    // Though `A`'s discriminant is 7 in `MySubSet`,
    // here it will be 4
    #[tagset(include(super::my_subset::MySubSet))]
    // `D`'s discriminant will be directly after `C`'s (8 -> 9)
    #[tagset(D)]
    pub struct MySet;
}
fn main() {
    let a: my_subset::MySubSet = my_subset::A.into();
    let b: my_subset::MySubSet = my_subset::B.into();
    assert_eq!(a.discriminant(), 7);
    assert_eq!(b.discriminant(), 11);
    use tagset::TagSet as _;
    let a: my_set::MySet = my_subset::A.into();
    let b: my_set::MySet = my_subset::B.into();
    let c: my_set::MySet = my_set::C.into();
    let d: my_set::MySet = my_set::D.into();
    assert_eq!(a.discriminant(), 4);
    assert_eq!(b.discriminant(), 8);
    assert_eq!(c.discriminant(), 3);
    assert_eq!(d.discriminant(), 9);
}

Metadata

tagset supports adding metadata to traits to control default implementation.
To add metadata to a trait, use the [tagset_meta] attribute, then use #[meta(...)] attributes where desired.

default

default can be applied to associated types or associated functions.
When applied to an associated type, it takes a type path which will serve as the default for any implementing set. In this case, it is not necessary to provide an override in the set impl.
When applied to a trait function, it takes a function body. The macros mentioned in the overrides section are available here as well.
Note: telety aliases all types used in the function body so they are usable at the set definition without importing them, but it cannot alias use items or values. Those must be textually valid at the location of the set definition.

bounds

By default, the set implements traits when all its variants implement the trait. This may be too strict or too lax for some default implementations. The bounds meta attribute takes where predicates to use as the baseline bounds to implement the trait for a set. See the type bounds section for special bounds syntax.

pub mod my_trait {
    #[telety::telety(crate::my_trait)]
    #[tagset::tagset_meta]
    // We don't need any bounds for our default impl
    #[meta(bounds())]
    pub trait MyTrait {
        #[meta(default(std::convert::Infallible))]
        type Error;
        #[meta(default {
            match_by_value!(self, value => Ok(std::any::type_name_of_val(value)))
        })]
        fn name(&self) -> Result<&'static str, Self::Error>;
    }
}
pub mod my_set {
    # use tagset::tagset;
    // Because MyTrait has no default bounds, these don't need to implement MyTrait
    pub struct A;
    pub struct B;
    
    #[tagset(impl super::my_trait::MyTrait)]
    #[tagset(A)]
    #[tagset(B)]
    pub struct MySet;
}
fn main() {
    use my_trait::MyTrait as _;
    let a: my_set::MySet = my_set::A.into();
    let b: my_set::MySet = my_set::B.into();
    assert_eq!(a.name().unwrap().split("::").last().unwrap(), "A");
    assert_eq!(b.name().unwrap().split("::").last().unwrap(), "B");
}

derive

tagset currently uses a private internal type when implementing the set. Because of this, derives do not work as-is. Wrap them in a tagset attribute:

pub mod my_set {
    # use tagset::tagset;
    #[derive(Clone)]
    pub struct A;
    #[tagset(derive(Clone))]
    #[tagset(A)]
    pub struct MySet;
}
fn main() {
    let before: my_set::MySet = my_set::A.into();
    let after = before.clone();
}

serde

When the serde feature is enabled, tagset includes Serialize and Deserialize proxies with default implementations. Currently, the Deserialize implementation requires also implementing the trait [serde::DeserializeFromDiscriminant], but this may be automatic in the future.

# // This does not work as a doctest for some reason, see tests/serde_roundtrip.rs
# use tagset::tagset;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Number(pub i32);
#[derive(serde::Serialize, serde::Deserialize)]
pub struct Text(pub String);
#[tagset(impl tagset::proxy::serde::Serialize)]
#[tagset(impl<'de> tagset::serde::DeserializeFromDiscriminant<'de>)]
#[tagset(impl<'de> tagset::proxy::serde::Deserialize<'de>)]
#[tagset(Number)]
#[tagset(Text)]
pub struct MySet;
fn main() -> anyhow::Result<()> {
    let value: MySet = Text("asdf".to_string()).into();
    let serialized = serde_json::to_string(&value)?;
    let deserialized: MySet = serde_json::from_str(&*serialized)?;
    
    let text: Text = deserialized.try_into().unwrap_or_else(|_| unreachable!());
    assert_eq!(text.0, "asdf");
    Ok(())
}

The default (de)serialization implementation structures the set as a 2-element tuple. The first element is the discriminant, the second is the inner variant.

Commit count: 0

cargo fmt