Search code examples
rustrust-macros

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!


Solution

  • 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