Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an Actor custom derive macro #163

Merged
merged 11 commits into from
Aug 12, 2022
Merged
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ wasm-bindgen-futures = { version = "0.4", optional = true, default-features = fa
# Feature `instrumentation`
tracing = { version = "0.1.35", optional = true, default-features = false }

macros = { package = "xtra-macros", path = "./macros", optional = true }

[dev-dependencies]
rand = "0.8"
smol = "1.1"
Expand All @@ -51,7 +53,6 @@ smol = ["dep:smol"]
tokio = ["dep:tokio"]
wasm_bindgen = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures"]
sink = ["dep:futures-sink", "futures-util/sink"]

[[example]]
name = "basic_tokio"
required-features = ["tokio"]
Expand Down Expand Up @@ -109,7 +110,7 @@ name = "instrumentation"
required-features = ["tokio", "instrumentation"]

[workspace]
members = ["examples/basic_wasm_bindgen"]
members = ["examples/basic_wasm_bindgen", "macros", "macros-test"]

[package.metadata.docs.rs]
features = ["async_std", "smol", "tokio", "wasm_bindgen"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Keep in mind that `xtra` has a MSRV of 1.60.0.
- `wasm_bindgen`: enables integration with [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen), and particularly its futures crate.
- `instrumentation`: Adds a dependency on `tracing` and creates spans for message sending and handling on actors.
- `sink`: Adds `Address::into_sink` and `MessageChannel::into_sink`.
- `macros`: Enables the `Actor` custom derive macro.

## Latest Breaking Changes
To see the breaking changes for each version, see [here](https://github.com/Restioson/xtra/blob/master/BREAKING-CHANGES.md).
Expand Down
13 changes: 13 additions & 0 deletions macros-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "macros-test"
version = "0.1.0"
edition = "2021"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
xtra = { path = "../", features = ["macros"] }

[dev-dependencies]
trybuild = "1.0.64"
7 changes: 7 additions & 0 deletions macros-test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# macros-test

This is a testing crate for the macros provided by xtra as they are re-exported from the main `xtra` library.

Moving the tests into a separate crate allows us to define test utils in the crate's `lib.rs` file and ensures the user-facing public macro API is as we expect.
Users are meant to access the macros through the main `xtra` crate.
The `xtra-macros` crate is an implementation detail that is enforced by `cargo` because proc-macros need to be in their own crate.
5 changes: 5 additions & 0 deletions macros-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub fn assert_actor<A>()
where
A: xtra::Actor,
{
}
Empty file added macros-test/tests/fail/.gitkeep
Empty file.
6 changes: 6 additions & 0 deletions macros-test/tests/pass/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[derive(xtra::Actor)]
struct MyActor;

fn main() {
macros_test::assert_actor::<MyActor>()
}
28 changes: 28 additions & 0 deletions macros-test/tests/pass/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#[derive(xtra::Actor)]
struct MyActor<A, B> {
phantom: std::marker::PhantomData<(A, B)>
}

#[derive(xtra::Actor)]
struct MyActorWithBounds<A: Bar, B> {
phantom: std::marker::PhantomData<(A, B)>
}

#[derive(xtra::Actor)]
struct MyActorWithWhere<A, B> where A: Bar {
phantom: std::marker::PhantomData<(A, B)>
}

struct Foo;

trait Bar { }

impl Bar for &'static str {

}

fn main() {
macros_test::assert_actor::<MyActor<&'static str, Foo>>();
macros_test::assert_actor::<MyActorWithBounds<&'static str, Foo>>();
macros_test::assert_actor::<MyActorWithWhere<&'static str, Foo>>();
}
6 changes: 6 additions & 0 deletions macros-test/tests/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/pass/*.rs");
t.compile_fail("tests/fail/*.rs");
}
14 changes: 14 additions & 0 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "xtra-macros"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
syn = "1"
quote = "1"
proc-macro2 = "1"
49 changes: 49 additions & 0 deletions macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_quote, DeriveInput, GenericParam, WherePredicate};

#[proc_macro_derive(Actor)]
pub fn actor_derive(input: TokenStream) -> TokenStream {
let derive_input = syn::parse::<DeriveInput>(input).expect("macro to be used as custom-derive");

let send_and_static_bounds = send_and_static_bounds(&derive_input);
let actor_ident = derive_input.ident;
let (impl_generics, type_generics, where_clause) = derive_input.generics.split_for_impl();
let where_clause = match where_clause.cloned() {
None => parse_quote! { where #(#send_and_static_bounds),* },
Some(mut existing) => {
existing.predicates.extend(send_and_static_bounds);

existing
}
};

quote! {
#[xtra::prelude::async_trait]
impl #impl_generics xtra::Actor for #actor_ident #type_generics #where_clause {
type Stop = ();

async fn stopped(self) { }
}
}
.into()
}

/// Generics a `: Send + 'static` predicate for each type parameter present in the generics.
fn send_and_static_bounds(input: &DeriveInput) -> Vec<WherePredicate> {
input
.generics
.params
.iter()
.filter_map(|gp| match gp {
GenericParam::Type(tp) => Some(&tp.ident),
GenericParam::Lifetime(_) => None,
GenericParam::Const(_) => None,
})
.map(|ident| {
parse_quote! {
#ident: Send + 'static
}
})
.collect::<Vec<WherePredicate>>()
}
33 changes: 33 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,39 @@ pub mod refcount {
pub use crate::inbox::tx::TxWeak as Weak;
}

/// Provides a default implementation of the [`Actor`] trait for the given type with a [`Stop`](Actor::Stop) type of `()` and empty lifecycle functions.
///
/// The [`Actor`] custom derive takes away some boilerplate for a standard actor:
///
/// ```rust
/// #[derive(xtra::Actor)]
/// pub struct MyActor;
/// #
/// # fn assert_actor<T: xtra::Actor>() { }
/// #
/// # fn main() {
/// # assert_actor::<MyActor>()
/// # }
/// ```
/// This macro will generate the following [`Actor`] implementation:
///
/// ```rust,no_run
/// # use xtra::prelude::*;
/// pub struct MyActor;
///
/// #[async_trait]
/// impl xtra::Actor for MyActor {
/// type Stop = ();
///
/// async fn stopped(self) { }
/// }
/// ```
///
/// Please note that implementing the [`Actor`] trait is still very easy and this macro purposely does not support a plethora of usecases but is meant to handle the most common ones.
/// For example, whilst it does support actors with type parameters, lifetimes are entirely unsupported.
#[cfg(feature = "macros")]
pub use macros::Actor;

/// Defines that an [`Actor`] can handle a given message `M`.
///
/// This is an [`async_trait`](https://docs.rs/async-trait), so implementations should
Expand Down