如何在Rust中使用宏生成复杂的枚举变体

3

我正在编写一个解析OVPN配置文件的小型库。OVPN配置文件具有以下格式:

command arg1 arg2
othercommand arg1 arg2

有一组固定的命令,其中一些具有可选参数。我想将解析后的命令表示为枚举类型。因此,以上内容可能会被表示为:

enum ConfigDirective{
    Command{arg1: String},
    OtherCommand{arg1: String, optinal_arg1: Option<String>},
}

fn parse_line(command: String, args: Vec<String>) -> ConfigDirective {
    match command {
        "command" => ConfigDirective::Command{arg1: args[0]},
        "other_command" => ConfigDirective:OtherCommand{arg1: args[0], optional_arg1: args.get(1),
    }
}

我喜欢这种结构,但有很多可能的命令(大约有280个)。所以我想写一个宏来生成大部分样板代码。理想情况下,我会写出以下内容:

define_config_directive!{
    {command => "command1", rust_name => CommandOne, args => [arg1], optional_args => []},    
    {command => "other_command", rust_name => OtherCommand, args => [arg1], optional_args => [optional_arg1]},
}

到目前为止,我能够找到的最接近的翻译是这个:
macro_rules! define_config_directives {
    ($({
        rust_name => $rust_name:ident,
        required => [$($required:ident),*],
        optional => [$($optional:ident),*]
    }),*) => {
        #[derive(PartialEq, Eq, Debug)]
        pub enum ConfigDirective {
            $($rust_name{
                $($required: String),*,
                $($optional: Option<String>),*,
            }),*
        }
    };
}

我有几个问题:

  1. 我不知道如何在这个宏中实现parse_line函数,我需要迭代每个必需参数,编写一些代码以从行中提取相应的参数,并对可选参数执行相同的操作。
  2. 我不知道如何处理没有任何参数的情况,理想情况下,那将是一个没有字段的简单枚举变体。

有人知道是否有办法在稳定的Rust上解决这个问题吗?还是我应该使用Python脚本生成代码?


2
注意:宏和外部脚本之间的一个中间步骤是build.rs文件。当使用cargo build命令时,cargo会首先编译并执行build.rs文件(如果存在),然后再编译整个crate的其余部分,因此您可以轻松地使用build.rs文件生成Rust代码,而无需涉及任何第三方工具或makefile。 - undefined
1个回答

10
这是一个有些特殊的情况。首先,您想要以不同的方式处理输入的部分,这对于宏来说并不好。更糟糕的是,您想要在生成枚举变量的同时进行这样的操作,而宏也无法胜任。据我所知,只有一种方法可以解决这个问题:完全使用下推生成。
简单来说,将其分解为简单的匹配步骤,每个步骤处理一件事,并将该事物的输出添加到累加器中(在本例中为$eout$pout)。当您没有输入时,请将累加器转储到输出中。
macro_rules! define_config_directive {
    // Start rule.
    // Note: `$(,)*` is a trick to eat any number of trailing commas.
    ( $( {$($cmd:tt)*} ),* $(,)*) => {
        // This starts the parse, giving the initial state of the output
        // (i.e. empty).  Note that the commands come after the semicolon.
        define_config_directive! { @parse {}, (args){}; $({$($cmd)*},)* }
    };

    // Termination rule: no more input.
    (
        @parse
        // $eout will be the body of the enum.
        {$($eout:tt)*},
        // $pout will be the body of the `parse_line` match.
        // We pass `args` explicitly to make sure all stages are using the
        // *same* `args` (due to identifier hygiene).
        ($args:ident){$($pout:tt)*};
        // See, nothing here?
    ) => {
        #[derive(PartialEq, Eq, Debug)]
        enum ConfigDirective {
            $($eout)*
        }

        fn parse_line(command: &str, $args: &[&str]) -> ConfigDirective {
            match command {
                $($pout)*
                _ => panic!("unknown command: {:?}", command)
            }
        }
    };

    // Rule for command with no arguments.
    (
        @parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
        {
            command: $sname:expr,
            rust_name: $rname:ident,
            args: [],
            optional_args: [] $(,)*
        },
        $($tail:tt)*
    ) => {
        define_config_directive! {
            @parse
            {
                $($eout)*
                $rname,
            },
            ($pargs){
                $($pout)*
                $sname => ConfigDirective::$rname,
            };
            $($tail)*
        }
    };

    // Rule for other commands.
    (
        @parse {$($eout:tt)*}, ($pargs:ident){$($pout:tt)*};
        {
            command: $sname:expr,
            rust_name: $rname:ident,
            args: [$($args:ident),* $(,)*],
            optional_args: [$($oargs:ident),* $(,)*] $(,)*
        },
        $($tail:tt)*
    ) => {
        define_config_directive! {
            @parse
            {
                $($eout)*
                $rname { $( $args: String, )* $( $oargs: Option<String>, )* },
            },
            ($pargs){
                $($pout)*
                $sname => {
                    // This trickery is because macros can't count with
                    // regular integers.  We'll just use a mutable index
                    // instead.
                    let mut i = 0;
                    $(let $args = $pargs[i].into(); i += 1;)*
                    $(let $oargs = $pargs.get(i).map(|&s| s.into()); i += 1;)*
                    let _ = i; // avoid unused assignment warnings.

                    ConfigDirective::$rname {
                        $($args: $args,)*
                        $($oargs: $oargs,)*
                    }
                },
            };
            $($tail)*
        }
    };
}

define_config_directive! {
    {command: "command1", rust_name: CommandOne, args: [arg1], optional_args: []},    
    {command: "other_command", rust_name: OtherCommand, args: [arg1], optional_args: [optional_arg1]},
}

fn main() {
    println!("{:?}", parse_line("command1", &["foo"]));
    println!("{:?}", parse_line("other_command", &["foo"]));
    println!("{:?}", parse_line("other_command", &["foo", "bar"]));
}

不,你无法避免累加器的问题,因为宏无法直接扩展到枚举变量。因此,你必须一次性扩展整个枚举定义。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接