跳转至

crates\nfarms-plugin\src\plugin.rs

源码路径:crates\nfarms-plugin\src\plugin.rs · 145 行

//! 插件 trait 与错误类型。

use std::fmt;

use crate::context::Context;

/// 一个功能单元。
///
/// 实现者通常是个**无状态**的结构体(要存状态就放进自己提供的服务里),
/// 这样 [`Plugin::apply`] 可以安全地反复调用(cordis 的插件也要求幂等)。
pub trait Plugin: Send + Sync + 'static {
    /// 插件名。界面上要展示,而且同名插件只装一个 —— 取个能认出来的名字。
    fn name(&self) -> &str;

    /// 版本。
    ///
    /// **默认值是 `nfarms-plugin` 自己的版本,不是插件所在包的版本。**
    /// `env!("CARGO_PKG_VERSION")` 在展开它的 crate 里求值,而这行代码编译进
    /// `nfarms-plugin`;所以插件若在别的包里,默认值报的是框架的版本。
    /// 现在五个 crate 都是 `version.workspace = true`,取值恰好相同,现象看不出来 ——
    /// 一旦有包单独升版本,界面上的插件版本就会串。
    ///
    /// 因此插件**只要不在 `nfarms-plugin` 包里,就应该自己实现这个函数**
    /// (一行 `env!("CARGO_PKG_VERSION")`,那个 `env!` 在插件自己的包里展开)。
    /// 反过来,测试桩和同包内的实现用默认值是合适的。
    ///
    /// # 例子
    ///
    /// ```
    /// use nfarms_plugin::{Context, Plugin, Result};
    ///
    /// /// 测试桩 / 同包实现:什么都不写,版本 = `nfarms-plugin` 的版本。
    /// struct Stub;
    /// impl Plugin for Stub {
    ///     fn name(&self) -> &str {
    ///         "stub"
    ///     }
    ///     fn apply(&self, _ctx: &Context) -> Result<()> {
    ///         Ok(())
    ///     }
    /// }
    /// assert_eq!(Stub.version(), env!("CARGO_PKG_VERSION"));
    ///
    /// /// 跨包插件:必须自己实现,否则上面那个值会冒充本插件的版本。
    /// struct Elsewhere;
    /// impl Plugin for Elsewhere {
    ///     fn name(&self) -> &str {
    ///         "elsewhere"
    ///     }
    ///     fn version(&self) -> &str {
    ///         "0.2.0" // 真实写法是 env!("CARGO_PKG_VERSION"),在这里展开
    ///     }
    ///     fn apply(&self, _ctx: &Context) -> Result<()> {
    ///         Ok(())
    ///     }
    /// }
    /// assert_eq!(Elsewhere.version(), "0.2.0");
    /// ```
    fn version(&self) -> &str {
        env!("CARGO_PKG_VERSION")
    }

    /// 依赖的服务名。**没满足不会报错**:插件先挂起(Pending),
    /// 等哪天有插件把缺的服务 `provide` 出来了,内核自动把它装上。
    ///
    /// 这正是「加功能不用改老代码」的关键 —— 老插件可以先加载,
    /// 它依赖的服务由更晚注册的插件提供。
    fn inject(&self) -> &[&'static str] {
        &[]
    }

    /// 装载。在这里 `provide` 自己的服务、注册自己的页面、订阅事件。
    fn apply(&self, ctx: &Context) -> Result<()>;

    /// 卸载。默认什么都不做。
    ///
    /// 注意:内核**不会**替你摘掉已 provide 的服务(cordis 里作用域销毁会
    /// 连带清理,这里没做那么细)。所以插件自己 provide 的服务,自己负责
    /// 在 `dispose` 里置空或标记失效。
    fn dispose(&self, _ctx: &Context) -> Result<()> {
        Ok(())
    }
}

/// 插件返回的错误。带上插件名,免得界面上只看到一句没头没尾的话。
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginError {
    pub plugin: String,
    pub message: String,
}

impl PluginError {
    pub fn new(plugin: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            plugin: plugin.into(),
            message: message.into(),
        }
    }
}

impl fmt::Display for PluginError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}] {}", self.plugin, self.message)
    }
}

impl std::error::Error for PluginError {}

/// 插件 API 的结果类型。
pub type Result<T> = std::result::Result<T, PluginError>;

#[cfg(test)]
mod tests {
    use super::*;

    struct Noop;
    impl Plugin for Noop {
        fn name(&self) -> &str {
            "noop"
        }
        fn apply(&self, _ctx: &Context) -> Result<()> {
            Ok(())
        }
    }

    /// 默认实现只保证「有版本号」,不保证「是插件自己那个包的版本」——
    /// 它返回的是 `nfarms-plugin` 的版本(见 [`Plugin::version`] 的文档)。
    /// 这里断言的是这条**已写明的语义**,而不是拿它去证明跨包一致:
    /// 跨包的版本对齐只能在插件自己的包里断言(例如在 `nfarms-gui` 里
    /// 断 `PagePlugin::version() == env!("CARGO_PKG_VERSION")`),本 crate 测不到。
    #[test]
    fn default_version_is_the_framework_version() {
        let p = Noop;
        assert_eq!(p.name(), "noop");
        assert!(p.inject().is_empty());
        assert_eq!(p.version(), env!("CARGO_PKG_VERSION"));
    }

    #[test]
    fn error_message_carries_plugin_name() {
        let e = PluginError::new("weather", "取不到数据");
        assert_eq!(e.to_string(), "[weather] 取不到数据");
    }
}