#枚举 # #过程宏 #生成器 #实用工具 #用户定义

构建 enpow

生成用户定义枚举的方法,类似于从Option或Result中已知的方法

6个稳定版本

2.0.2 2023年10月3日
2.0.1 2023年2月16日
2.0.0 2022年9月7日
1.0.2 2022年9月1日
1.0.0 2022年8月31日

#180 in 构建工具

MIT/Apache

105KB
2K SLoC

EnPow

License

EnPow是一个过程宏crate,用于增强用户定义枚举,使其具有通常从标准库中的Result<T, E>Option<T>中已知的方法。它可以生成像fn is_<variant>(&self) -> boolfn unwrap_<variant>(self) -> <inner>这样的方法,支持具有命名或未命名的字段(或没有字段),以及泛型。有关支持的具体方法,请参阅enpow宏文档

此外,这个crate允许将每个枚举变体的相关数据提取到单独的结构体中,这可以在设计抽象语法树时使代码更加紧凑。有关详细信息,请参阅extract宏文档

也可以将这两个宏按正确顺序组合使用:首先是 extract,然后是 enpow。组合这两个宏可以避免为 RefMut 结构体变体生成单独的结构体,如以下用例演示中进一步解释的那样。不过,这两个宏也可以独立使用。

安装

⚠️ 支持的最小 Rust 工具链版本是 Rust 1.58.0

将以下内容添加到您的 Cargo.toml 中,以便将 enpow 作为依赖项添加到您的项目中。

[dependencies]
enpow = "~2.0.2"

或者,在您的 crate 根目录内运行以下命令。

cargo add enpow

快速入门

Enpow

要为枚举生成所有可用方法,导入 enpow 宏,并在枚举定义之前添加 #[enpow(All)]。您还可以选择性地生成某些方法。有关更多信息,请参阅以下用例演示或 enpow 宏的文档

use enpow::enpow;

#[enpow(All)]
pub enum IpAddress {
    None,
    V4(u8, u8, u8, u8),
    V6(String),
    Multi {
        v4: (u8, u8, u8, u8),
        v6: String,
    },
}

一些 生成的函数如下

  • fn v4(self) -> Option<(u8, u8, u8, u8)>
  • fn is_none(&self) -> bool
  • fn unwrap_v6(self) ->String
  • fn expect_multi(self, msg: &str) ->IpAddressMulti
  • ...

除了这些方法外,还需要生成以下结构体

pub struct IpAddressMulti {
    pub v4: (u8, u8, u8, u8),
    pub v6: String,
}

Extract

要提取枚举的所有变体到单独的类型中,导入 extract 宏,并在枚举定义之前添加 #[extract(All)]。您还可以选择性地提取某些变体。有关更多信息,请参阅以下用例演示或 extract 宏的文档

use enpow::extract;

#[extract(All)]
pub enum IpAddress {
    None,
    V4(u8, u8, u8, u8),
    V6(String),
    Multi {
        v4: (u8, u8, u8, u8),
        v6: String,
    },
}

这将生成以下数据类型

pub enum IpAddress {
    None(IpAddressNone),
    V4(IpAddressV4),
    V6(IpAddressV6),
    Multi(IpAddressMulti),
}

pub struct IpAddressNone;

pub struct IpAddressV4(pub u8, pub u8, pub u8, pub u8);

pub struct IpAddressV6(pub String);

pub struct IpAddressMulti {
    pub v4: (u8, u8, u8, u8),
    pub v6: String,
}

用例

以下代码描述了一个简单的支持不同日志级别的日志系统。然后我们创建一个示例日志并打印其中的所有错误。

/// A log entry
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
    /// A simple note without context
    Note(
        /// Note's message
        String
    ),
    /// A warning with a given context
    Warning(
        /// Warning's message
        String,
        /// Context of the warning
        C
    ),
    /// An error message with error code and context
    Error {
        /// Error message
        message: String,
        /// Context of the error
        context: C,
        /// Error code
        code: i16,
    },
}

/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
    /// Log entries
    entries: Vec<LogEntry<C>>,
}

impl<C: ToString + Clone> Log<C> {
    /// Collects all entries of type `LogEntry::Error` from the log
    pub fn get_errors(&self) -> Vec<LogEntry<C>> {
        self.entries.iter()
            .filter(|entry| match entry {
                LogEntry::Error { .. } => true,
                _ => false,
            })
            .cloned()
            .collect()
    }
}

/// Line number in source
type Line = usize;

// Create a sample log
let log = Log { entries: vec![
    LogEntry::Note("All fine 😊".into()),
    LogEntry::Warning("There might be an issue here 🤔".into(), 4),
    LogEntry::Error {
        message: "There _was_ an issue 😖".into(),
        context: 4,
        code: -1,
    },
    LogEntry::Error {
        message: "Follow up".into(),
        context: 12,
        code: -7,
    },
] };

// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
    eprintln!("Failed for the following reasons:");

    for error in errors {
        match error {
            LogEntry::Error { message, context: line, code } => {
                eprintln!("Error {code} at {line}: {message}");
            }
            _ => panic!("Expected to find a LogEntry::Error"),
        }
    }
}

此代码可以工作,但是每次都需要对每个日志条目的具体变体进行模式匹配,这略显冗长。

使用 enpow

这时,enpow 宏就派上用场了。它可以生成一些辅助方法,可以使我们的代码更加简洁。我们特别使用了 is_<variant>() (关键字 IsVar)和 unwrap_<variant>() (关键字 UnwrapVar)方法。

use enpow::enpow; // ℹ️

/// A log entry
#[enpow(IsVar, UnwrapVar)] // ℹ️
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
    /// A simple note without context
    Note(
        /// Note's message
        String
    ),
    /// A warning with a given context
    Warning(
        /// Warning's message
        String,
        /// Context of the warning
        C
    ),
    /// An error message with error code and context
    Error {
        /// Error message
        message: String,
        /// Context of the error
        context: C,
        /// Error code
        code: i16,
    },
}

/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
    /// Log entries
    entries: Vec<LogEntry<C>>,
}

impl<C: ToString + Clone> Log<C> {
    /// Collects all entries of type `LogEntry::Error` from the log
    pub fn get_errors(&self) -> Vec<LogEntry<C>> {
        self.entries.iter()
            .filter(|entry| entry.is_error()) // ℹ️
            .cloned()
            .collect()
    }
}

/// Line number in source
type Line = usize;

// Create a sample log
let log = Log { entries: vec![
    LogEntry::Note("All fine 😊".into()),
    LogEntry::Warning("There might be an issue here 🤔".into(), 4),
    LogEntry::Error {
        message: "There _was_ an issue 😖".into(),
        context: 4,
        code: -1,
    },
    LogEntry::Error {
        message: "Follow up".into(),
        context: 12,
        code: -7,
    },
] };

// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
    eprintln!("Failed for the following reasons:");

    for error in errors {
        let error = error.unwrap_error();  // ℹ️
        eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
    }
}

尽管这段代码已经很简洁了,但仍有一些粗糙的边缘。在收集所有错误时,它们仍然以没有类型系统保证只包含错误的 Vec<LogEntry> 形式返回。我们可以通过从 get_errors() 返回已构造的类型 LogEntryError<C> 来解决这个问题,以帮助 unwrap_error() 方法。

然而,目前代码中有多个(取决于生成的方法,最多可达四个)部分定义了相同的数据:在枚举变体 LogEntry::Error { .. } 中,以及刚才提到的自动生成的结构体 LogEntryError<C> 中。可能更希望将枚举变体提取到这个结构体中,并让变体只指向它,如下所示:LogEntry::Error(LogEntryError<C>)。这在处理需要引用变体关联数据的方时特别有用,因为到目前为止,需要为这些生成特殊的 LogEntryErrorRef<C>LogEntryErrorMut<C> 结构体。数据提取后,只需使用 &LogEntryError<C>&mut LogEntryError<C> 即可。

使用 extract

在这里,extract 宏派上用场,它会自动为我们完成这项工作。我们告诉宏为每个具有一个以上未命名字段(关键字 Unnamed)或命名字段(关键字 Named)的变体进行提取。这将自动将受影响的两个变体改为 LogEntry::Warning(LogEntryWarning<C>)LogEntry::Error(LogEntryError<C>)。然后我们可以使用生成的类型 LogEntryError<C> 来保证 get_errors() 实际上只返回错误。注意,尽管枚举代码没有手动更改,但日志条目的构建方式也发生了变化。extract 还自动实现了 From 特性,以将提取类型的实例转换为相应的枚举变体。

此外,我们使用方法 <variant>_as_ref()(关键字 VarAsRef)来使收集所有错误条目和解包它们更加简洁。为了使自动生成的 LogEntryError<C> 结构体的克隆工作,我们添加了 inner(derive(Clone)) 属性。

⚠️ 宏顺序:当结合两个宏时,enpow 必须放在 after extract 之后才能正确工作。如果辅助属性 inner 放在 extractenpow 之间,它将只对 extract 有效。如果它放在 extractenpow 之后,它将同时对两个宏有效。此外,正常的 derive 宏必须放在 after extract 之后才能正确工作。

use enpow::{enpow, extract}; // ℹ️

/// A log entry
#[extract(Unnamed, Named)] // ℹ️
#[enpow(VarAsRef)]         //
#[inner(derive(Clone))]    //
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
    /// A simple note without context
    Note(
        /// Note's message
        String
    ),
    /// A warning with a given context
    Warning(
        /// Warning's message
        String,
        /// Context of the warning
        C
    ),
    /// An error message with error code and context
    Error {
        /// Error message
        message: String,
        /// Context of the error
        context: C,
        /// Error code
        code: i16,
    },
}

/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
    /// Log entries
    entries: Vec<LogEntry<C>>,
}

impl<C: ToString + Clone> Log<C> {
    /// Collects all entries of type `LogEntry::Error` from the log
    pub fn get_errors(&self) -> Vec<LogEntryError<C>> { // ℹ️
        self.entries.iter()
            .filter_map(|entry| entry.error_as_ref())   // ℹ️
            .cloned()
            .collect()
    }
}

/// Line number in source
type Line = usize;

// Create a sample log
let log = Log { entries: vec![
    LogEntry::Note("All fine 😊".into()),
    LogEntry::from(LogEntryWarning( // ℹ️
        "There might be an issue here 🤔".into(),
        4,
    )),
    LogEntry::from(LogEntryError { // ℹ️
        message: "There _was_ an issue 😖".into(),
        context: 4,
        code: -1,
    }),
    LogEntry::from(LogEntryError { // ℹ️
        message: "Follow up".into(),
        context: 12,
        code: -7,
    }),
] };

// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
    eprintln!("Failed for the following reasons:");

    for error in errors {
        // ℹ️
        eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
    }
}

最后,我们通过定义另一个命名模式来移除生成结构体的长名称。为此,使用具有参数 type_nametype_names 的相同 inner() 辅助属性。此外,使用 inner(),还可以更改变体名称在方法中的显示方式 method_name="...",并且可以为单个变体添加自动派生。

use enpow::{enpow, extract};

/// A log entry
#[extract(Unnamed, Named)]
#[enpow(VarAsRef)]
#[inner(type_names=Var, derive(Clone))] // ℹ️
#[derive(Clone)]
pub enum LogEntry<C: ToString + Clone> {
    /// A simple note without context
    Note(
        /// Note's message
        String
    ),
    /// A warning with a given context
    Warning(
        /// Warning's message
        String,
        /// Context of the warning
        C
    ),
    /// An error message with error code and context
    Error {
        /// Error message
        message: String,
        /// Context of the error
        context: C,
        /// Error code
        code: i16,
    },
}

/// Application log for a certain context type
pub struct Log<C: ToString + Clone> {
    /// Log entries
    entries: Vec<LogEntry<C>>,
}

impl<C: ToString + Clone> Log<C> {
    /// Collects all entries of type `LogEntry::Error` from the log
    pub fn get_errors(&self) -> Vec<Error<C>> { // ℹ️
        self.entries.iter()
            .filter_map(|entry| entry.error_as_ref())
            .cloned()
            .collect()
    }
}

/// Line number in source
type Line = usize;

// Create a sample log
let log = Log { entries: vec![
    LogEntry::Note("All fine 😊".into()),
    LogEntry::from(Warning( // ℹ️
        "There might be an issue here 🤔".into(),
        4,
    )),
    LogEntry::from(Error { // ℹ️
        message: "There _was_ an issue 😖".into(),
        context: 4,
        code: -1,
    }),
    LogEntry::from(Error { // ℹ️
        message: "Follow up".into(),
        context: 12,
        code: -7,
    }),
] };

// Get and print all errors
let errors = log.get_errors();
if !errors.is_empty() {
    eprintln!("Failed for the following reasons:");

    for error in errors {
        eprintln!("Error {} at {}: {}", error.code, error.context, error.message);
    }
}

这只是一个快速入门示例,用于理解该crate的使用和用法。有关详细信息,请参阅宏的文档。

灵感和替代方案

虽然这个crate的第一个计划仅限于简单的 unwrap 方法等,但crate variantly 对此想法进行了很好的扩展。它可以看作是这个crate的替代品,具有部分不同的功能集。

另一个具有部分不同功能集的替代方案是 enum_variant_type。它的主要重点是为每个枚举变体生成单独的类型。




贡献

除非你明确表示,否则你提交的任何旨在包含在作品中的贡献,如Apache-2.0许可中定义的,都应按以下方式双重许可,而无需任何附加条款或条件。

许可证

© 2022 Florian Köhler。

此项目可按你的选择许可

此项目的SPDX许可证标识符为 MIT OR Apache-2.0


授权来源于 arnavyc/dual-licensed-mit-apache

依赖项

约1.5MB
约35K SLoC