Skip to content

Macro-based delegation for enums and structs.

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

arcane-rs/delegation

delegation crate

crates.io Rust 1.81+ Unsafe Forbidden
CI Rust docs

API Docs | Changelog

Macro-based delegation for enums and structs.

NOTE: This crate is a hard fork and THE successor of the unreleased enum_delegate 0.3 crate rewrite, which fell to be unmaintained.

How to use it

use delegation::delegate;

#[delegate(for(LastName))]
trait AsStr {
    fn as_str(&self) -> &str;
}

impl AsStr for String {
    fn as_str(&self) -> &str {
        self
    }
}

#[delegate(derive(AsStr))]
struct FirstName(String);

#[delegate]
struct LastName {
    name: String,
}

#[delegate(derive(AsStr))]
enum Name {
    First(FirstName),
    Last(LastName),
}

let name = Name::First(FirstName("John".to_string()));
assert_eq!(name.as_str(), "John");

let name = Name::Last(LastName {
    name: "Doe".to_string(),
});
assert_eq!(name.as_str(), "Doe");

Generics

In some cases, a trait or a type requires additional generic parameters to implement delegation. For this case, macro provides for<..> and where syntax for #[delegate(derive(..))]/#[delegate(for(..))] attribute arguments. Specified generics will be merged with the existing ones, provided by the trait/type definition.

use delegation::delegate;

#[delegate(for(
    for<U> Case2<U>
    where
        U: Named<N> + 'static,
))]
trait Named<N> {
    fn name(&self) -> N;
}

struct User(String);
impl Named<String> for User {
    fn name(&self) -> String {
        self.0.clone()
    }
}

#[delegate(derive(
    for<N> Named<N>
    where
        U: Named<N> + 'static,
))]
enum Case1<U> {
    User(U),
}

#[delegate]
struct Case2<U>(U);

#[delegate(derive(
   Named<String>
   where
       U: Named<String> + 'static,
))]
enum Case3<U> {
    Case1(Case1<U>),
    Case2(Case2<U>),
}

let user1 = Case1::User(User("Alice".to_string()));
assert_eq!(user1.name(), "Alice");

let user2 = Case2(User("Bob".to_string()));
assert_eq!(user2.name(), "Bob");

let user3 = Case3::Case1(Case1::User(User("Charlie".to_string())));
assert_eq!(user3.name(), "Charlie");

External types

Because the both sides of the delegation should be marked with the #[delegate] attribute, it's impossible to make external type delegatable. To handle this, the macro provides the #[delegate(as = my::Def)] attribute argument for struct fields and enum variants. It uses the provided type as known declaration of some external type. Provided type should be crate-local, and marked with the #[delegate] macro, and to provide an infallible conversion from external type (including reference-to-reference one).

use delegation::{
    private::Either, // non-public, but OK for showcase.
    delegate
};

#[delegate]
trait AsStr {
    fn as_str(&self) -> &str;
}

impl AsStr for String {
    fn as_str(&self) -> &str {
        self
    }
}

#[delegate(derive(AsStr))]
enum EitherDef {
    Left(String),
    Right(String),
}

impl<'a> From<&'a mut Either<String, String>> for &'a mut EitherDef {
    fn from(t: &'a mut Either<String, String>) -> Self {
        #[expect(unsafe_code, reason = "macro expansion")]
        unsafe {
            &mut *(t as *mut Either<String, String> as *mut EitherDef)
        }
    }
}

impl<'a> From<&'a Either<String, String>> for &'a EitherDef {
    fn from(t: &'a Either<String, String>) -> Self {
        #[expect(unsafe_code, reason = "macro expansion")]
        unsafe {
            &*(t as *const Either<String, String> as *const EitherDef)
        }
    }
}

impl From<Either<String, String>> for EitherDef {
    fn from(t: Either<String, String>) -> Self {
        match t {
            Either::Left(t) => EitherDef::Left(t),
            Either::Right(t) => EitherDef::Right(t),
        }
    }
}

#[delegate(derive(AsStr))]
struct EitherString(#[delegate(as = EitherDef)] Either<String, String>);

let left = EitherString(Either::Left("left".to_string()));
let right = EitherString(Either::Right("right".to_string()));
assert_eq!(left.as_str(), "left");
assert_eq!(right.as_str(), "right");

External traits

Because the both sides of the delegation should be marked with the #[delegate] attribute, it's impossible to make an external trait delegatable. To handle this, the macro provides the #[delegate(as = my::Def)] attribute argument for traits. It uses the provided trait as known declaration of some external trait. With this argument, the macro will generate a wrapper type implementing the external trait on it, with the name of the expanded "declaration" trait. By using this wrapper type in #[delegate(derive(ext::Trait as my::TraitDef))] argument, you can delegate external trait to your type.

use delegation::delegate;

#[delegate(as = AsRef)]
trait AsRefDef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

#[delegate]
trait AsStr {
    fn as_str(&self) -> &str;
}

impl AsStr for String {
    fn as_str(&self) -> &str {
        self
    }
}

#[delegate(as = AsStr)]
trait AsStrDef {
    fn as_str(&self) -> &str;
}

#[delegate(derive(
    AsRef<str> as AsRefDef,
    AsStr as AsStrDef,
))]
enum Name {
    First(String),
}

let name = Name::First("John".to_string());
assert_eq!(name.as_ref(), "John");
assert_eq!(name.as_str(), "John");

How it works

Crate provides several definitions:

  • delegate macro - derives trait on a new-type struct or enum, invoking it on its inner type.
  • Convert trait - converts enum or struct to a type representing "any of its variants".
  • "wrapper" type - some type used as a proxy to avoid blanket impls and orphan rules problems.

#[delegate] expansion on type

Implements the Convert trait for an enum/struct, which allows to convert it to "any of its variants" type.

use delegation::delegate;

#[delegate]
enum Name {
    First(FirstName),
    Last {
        name: LastName,
    },
}

generates

// NOTE: Simplified for readability.

impl Convert for Name {
    type Output = Either<FirstName, LastName>;

    fn convert(self) -> Self::Output {
        match self {
            Name::First(first_name) => Either::Left(first_name),
            Name::Last { name } => Either::Right(name),
        }
    }
}

#[delegate] expansion on trait

Implements the trait for a "wrapper" type, with inner type implementing the Convert trait, which "any variant" implements the target trait. I.e. each method in the generated impl converts self to the "wrapper" and then to "any of its variants" and invokes the target trait method on it.

Also, it generates a declarative macro used to implement delegation of this trait for a type.

use delegation::delegate;

#[delegate]
trait AsStr {
    fn as_str(&self) -> &str;
}

generates

// NOTE: Simplified for readability.

// Implementation for "any variant of enum or struct" type.
impl<L: AsStr, R: AsStr> AsStr for Either<L, R> {
    fn as_str(&self) -> &str {
        match self {
            Either::Left(left) => left.as_str(),
            Either::Right(right) => right.as_str(),
        }
    }
}

// Implementation for a wrapper type, which inner type implements the `Convert` trait.
// Required to make external types work. In that case the `Wrapper` will be crate-local type.
impl<T> AsStr for Wrapper<T>
where
    T: Convert,
    T::Output: AsStr,
{
    fn as_str(&self) -> &str {
        let this = self.convert(); // convert type to "any of its variant".
        this.as_str() // call the method.
    }
}

// Definition of macro which implements trait for the provided type.
// It invokes when `for`/`derive` arguments are provided to the `#[delegate]` macro. 
macro_rules! AsStr {
    ($trait_path:path, $ty:ty, $wrapper:ty) => {
        impl $trait_path for $ty {
            fn as_str(&self) -> &str {
                Wrapper(self).convert().as_str()
            }
        }
    };
}

Limitations

  • Both struct/enum and trait should be marked with the #[delegate] macro attribute.
  • Struct or enum variant should contain only a single field.
  • Trait methods must have an untyped receiver.
  • Supertraits or Self trait/method bounds except marker traits like Sized, Send or Sync are not supported yet.
  • Associated types/constants are not supported yet.
  • Lifetimes in methods are limited to be early-bounded in some cases (see rust-lang/rust#87803).
  • Self type is limited to be used in methods return types.

Alternatives and similar crates

delegation was highly inspired by the enum_dispatch crate. It provides similar functionality, but has more limitations:

  • Supports only enums.
  • Using enum_dispatch between crates is impossible due to limitations of its design.
  • Order-dependent macro expansion (in some cases your code fails if items marked with the macro has different order than macro expects).

Derives a method to return a borrowed pointer to the inner value, cast to a trait object, using the enum_derive::EnumInnerAsTrait. Slower though, more similar to dynamic dispatch. Also, less ergonomic due do usage of function-like macros.

delegation is a hard fork and THE successor of the unreleased enum_delegate 0.3 crate rewrite, which fell to be unmaintained.

License

This crate is licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.