Macro for simplifying channel communication

Hey, still really new to rust! I tried to simplify some calling code, but I'm not really that happy with it. Maybe someone has better ideas or helpful feedback (even without macro?)...

#![allow(dead_code)]
#![allow(unused_macros)]
#![allow(unused)]

use tokio::sync::mpsc::{Sender, Receiver};
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use rustis::client::Client;

type Responder<T> = oneshot::Sender<T>;

pub enum Command {
    Get {
        key: String,
        resp: Responder<Option<String>>
    },
    Set {
        key: String,
        val: String,
        resp: Responder<Result<(),()>>
    }
}

#[derive(Clone)]
pub struct RedisCmdSubmitter {
    tx: Sender<Command>
}

struct RedisChannel {
    pub cmd_submit: RedisCmdSubmitter,
    rx: Receiver<Command>
}

impl RedisChannel {
    fn new(buffer_size: usize) -> RedisChannel {
        let (tx, mut rx) : (Sender<Command>, Receiver<Command>) = mpsc::channel(buffer_size);

        RedisChannel {
            cmd_submit: RedisCmdSubmitter {
                tx
            },
            rx
        }
    }
}

#[macro_export]
macro_rules! red_req {
    ($cmd:ident, $chan:expr, $($data_name:ident, $data_val:expr),*) => {{
        let (resp_tx, resp_rx) = oneshot::channel();
        $chan.send(Command::$cmd { $($data_name: $data_val),*, resp: resp_tx }).await.unwrap();
        resp_rx
    }};
}

#[cfg(test)]
mod tests {
    use super::*;
    use Command::*;

    #[tokio::test]
    async fn basic() {
        let mut chan = RedisChannel::new(32);
        let cmd_tx1 = chan.cmd_submit.clone().tx;
        let cmd_tx2 = chan.cmd_submit.clone().tx;

        let t1 = tokio::spawn(async move {
            let res = red_req!(Get, cmd_tx1, key, "test1".to_string()).await;
            println!("GOT 1 = {:?}", res);
        });
    
        let t2 = tokio::spawn(async move {
            let res = red_req!(Set, cmd_tx2, key, "test2".to_string(), val, "val2".to_string()).await;
            println!("GOT 2 = {:?}", res);
        });
    
        while let Some(cmd) = chan.rx.recv().await {
            match cmd {
                Get { key, resp } => {
                    println!("received req {key}");
                    let _ = resp.send(Some("()".to_string()));
                }
                Set { key, val, resp } => {
                    println!("received req {key}: {val}");
                    let _ = resp.send(Err(()));
                }
            }
        }

        // not really needed
        t1.await;
        t2.await;
    }
}


The idea is to have simple calling code like red_req!(Get, cmd_tx1, key, "test1".to_string()); as it is needed frequently. As you can see I wanted to get rid of the oneshot (response) management, but I'm not sure how it would e.g. work for nested structs inside the Command enum, maybe there are better macro options (would be nice to call it like e.g.:

{
    key: "key".to_string(),
    val: "val".to_string()
}

which would also allow nested structs)...

Dependencies:

[dependencies]
tokio = { version = "1", features = ["sync"] }
rustis = "0.12"

[dev-dependencies]
tokio = { version = "1", features = ["rt"] }

Anyone? :smiley:

Looks pretty similar to my article in actors. I think what I would recommend there, is that you have a handle struct and define ordinary methods there, instead of having the users use a macro to send messages.

Now, a macro may still be useful for writing the methods that send the messages, but I don't think you should use them for users of your actor.

Thanks for taking the time reading and answer. I actually read that article a while ago, but probably didn't understand it completely or wanted to use it in a not fitting context.
Will give it a try and see if I can improve it in this way!
Btw, back when I read it, I already liked the actor approach, so thanks for sharing... :slight_smile:

Yeah, you may notice that get_unique_id from the article is extremely similar to the contents of your macro.

I have sometimes heard questions from people saying that defining a lot of methods like get_unique_id becomes repetitive. Last time that happened, I suggested that they use a macro similar to yours to define their get_unique_id-like functions.

Yes, it's a cleaner interface to the calling code for sure and should be the preferred way.
On the other hand it means, someone would have to replicate the whole interface of the library (rustis in this example), which is sub optimal (but this issue is also in my approach).
It would be nice, if it would be possible to pass a function call definition. The idea would be to tell the wrapper code: call function XYZ on the rustis client with arguments A,B,C and give me R.
But not sure if that is even possible.