You can do that with tt-munching:
macro_rules! impl_cols {
(@build-tuple
( $($types:path,)* )
( $($ns:ident)::* )
( $col_name:ident, $($rest:tt)* )
) => {
impl_cols! { @build-tuple
(
$($types,)*
$($ns::)* $col_name,
)
( $($ns)::* )
( $($rest)* )
}
};
// Empty case
(@build-tuple
( $($types:path,)* )
( $($ns:ident)::* )
( )
) => {
( $($types,)* )
};
(
$($ns:ident)::*,
$($col_name:ident,)*
) => {
pub fn cols() -> impl_cols! { @build-tuple
( )
( $($ns)::* )
( $($col_name,)* )
} {
impl_cols! { @build-tuple
( )
( $($ns)::* )
( $($col_name,)* )
}
}
};
}
Playground.
Edit:
The reason why simple matching doesn't work is that you cannot concatenate paths in Rust macro (without the help of a procedural macro). This is because, quoting the reference (https://doc.rust-lang.org/reference/macros-by-example.html#transcribing):
When forwarding a matched fragment to another macro-by-example, matchers in the second macro will see an opaque AST of the fragment type. The second macro can't use literal tokens to match the fragments in the matcher, only a fragment specifier of the same type. The ident, lifetime, and tt fragment types are an exception, and can be matched by literal tokens.
This is true not only when forwarding to another macro, but also when forwarding to the compiler.
You want to build two different AST fragments: a Type
for the return type (each type in the tuple), and an Expression
for the body. That is, crate::foo::bar::A
(for instance) fulfill two roles: in the return type it is a type (specifically TypePath
), and in the body it is an expression (specifically PathExpression
).
If we look at the definition of both (TypePath
and PathExpression
), we see they're essentially equal to the following (ignoring irrelevant parts like generics and function paths):
Path :
::
? PathIdentSegment (::
PathIdentSegment)*
PathIdentSegment :
IDENTIFIER | super
| self
| Self
| crate
| $crate
If you are not familiar with EBNF notation, this means a list of identifiers (:ident
in macro_rules!
), separated by ::
s.
So, when you're doing something like:
macro_rules! concat_ns {
($ns:path, $type:ident) => {
fn my_fn() -> $ns :: $type { todo!() }
};
}
concat_ns!(crate::foo::bar, A)
Your macro invocation builds an AST similar to:
MacroInvocation
...
Path
PathIdentSegment `crate`
PathIdentSegment `foo`
PathIdentSegment `bar`
COMMA
IDENTIFIER `A`
Your macro wants to build an AST similar to:
Function
...
FunctionReturnType
Type
Path
<Insert metavariable $ns here>
<Insert metavariable $type here>
Which gives you:
Function
...
FunctionReturnType
Type
Path
Path
PathIdentSegment `crate`
PathIdentSegment `foo`
PathIdentSegment `bar`
PathIdentSegment `A`
But this is an invalid AST, since Path
can contain only PathIdentSegment
and not other Path
s! (Note: this is not the exact process, but it is more-or-less the same).
Now you also understand why the tt-munching solution works: there, we never create a Path
node, and just keep the raw identifiers. We can concatenate raw identifiers and create a single Path
from them (this is generally the reason tt-munchers are used: when we need to not use Rust macro's syntax fragment capturing abilites because we want to restore them after).