| Crates.io | tagset |
| lib.rs | tagset |
| version | 0.1.1 |
| created_at | 2025-07-10 05:44:20.62279+00 |
| updated_at | 2025-07-17 23:58:27.915394+00 |
| description | Easily create trait-dispatching sum types |
| homepage | |
| repository | https://github.com/colinjneville/tagset |
| max_upload_size | |
| id | 1745914 |
| size | 52,977 |
Easily create trait-dispatching sum types (TagSets).
/// 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() { }
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.).
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() { }
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);
}
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,
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);
}
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 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.
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");
}
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();
}
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.