Daniel Porteous
Daniel Porteous

Reputation: 6353

How to match dot in macro and reconstruct original set of tokens?

I'm trying to write a macro that will expand this:

let res = log_request_response!(client.my_call("friend".to_string(), 5));

Into this:

let res = {
    debug!("Request: {}", args_separated_by_commas);
    let res = client.my_call("friend".to_string(), 5);
    debug!("Response: {}", res);
    res
};

My attempt so far is something like this:

#[macro_export]
macro_rules! log_request_response_to_scuba {
    ($($client:ident)?.$call:ident($($arg:expr),*);) => {
        let mut s = String::new();
        $(
            {
                s.push_str(&format!("{:?}, ", $arg));
            }
        )*
        s.truncate(s.len() - 2);
        debug!("Request: {}", s);
        // Somehow reconstruct the entire thing with res = at the start.
        debug!("Response: {}", res);
        res
    };
}

But this fails to compile:

error: macro expansion ignores token `{` and any following
  --> src/main.rs:10:13
   |
10 |             {
   |             ^
...
39 |     let res = log_request_response_to_scuba!(client.my_call(hey, 5));
   |               ------------------------------------------------------ caused by the macro expansion here
   |
   = note: the usage of `log_request_response_to_scuba!` is likely invalid in expression context

If I remove the . in between the client and call match it throws a different error about an ambiguous match (which makes sense).

So my first nitty gritty question is how do I match a dot? To me this match looks correct but apparently not.

Beyond that any help with making a macro that does what I want would be great. If it were a regex I'd just want this:

.*\((.*)\).*

Where I just capture the stuff inside the parentheses and split them. Then I use the 0th capture group to get the whole thing.

Thanks!

Upvotes: 2

Views: 717

Answers (1)

Elias Holzmann
Elias Holzmann

Reputation: 3679

The error message is not because you are matching the dot somehow wrong, but because you are not returning an expression. You want to return this:

{
    debug!("Request: {}", args_separated_by_commas);
    let res = client.my_call("friend".to_string(), 5);
    debug!("Response: {}", res);
    res
};

However, your macro currently returns something more akin to this:

debug!("Request: {}", args_separated_by_commas);
let res = client.my_call("friend".to_string(), 5);
debug!("Response: {}", res);
res

Note the missing curly braces. This can be remedied quite easily by enclosung the complete transcriber part in braces.


I am not sure why client is optional in your matcher. I assume that you want to optionally allow the user of the macro to either call a function or a method on some variable. Is that correct? If yes, then your code currently does not allow that – it matches client.my_call(...) as well as .some_function(...), but NOT some_function(...) (note the removed space from the beginning). To do what you want, you could match on $variable:ident$(.$field:ident)? – note that the dot is here optional as well – or even better $variable:ident$(.$field:ident)* to allow to call a method on a field of a field of a loval variable (so, something like variable.sub_struct.do_something().


The resulting code with some examples:

macro_rules! log_request_response {
    ($variable:ident$(.$field:ident)*($($arg:expr),*)) => {
        {
            let mut s = String::new();
            $(
                {
                    s.push_str(&format!("{:?}, ", $arg));
                }
            )*
            s.truncate(s.len() - 2);
            // using println! here because I don't want to set up logging infrastructure
            println!("Request: {}", s);
            let res = $variable$(.$field)*($($arg),*);
            println!("Response: {}", res);
            res
        }
    };
}

fn test_func(_: String, i: i32) -> i32 {
    i
}

struct TestStruct;

impl TestStruct {
    fn test_method(&self, _: String, i: i32) -> i32 {
        i
    }
}

fn main() {
    let _ = log_request_response!(TestStruct.test_method("friend".to_string(), 5));
    let _ = log_request_response!(test_func("friend".to_string(), 5));
}

Playground

Upvotes: 5

Related Questions