ek-core 0.4.3

Business-agnostic plugin host and actor-over-bus runtime on top of ek-event: capability index with runtime service discovery, per-plugin actor workers, dynamic add/remove with lifecycle broadcasts, one-shot ask/ask_service with contract gates, panic quarantine, five-step close.
Documentation
//! 通用自检闸的自用验证(最小假插件过 shape+deploy 双闸;
//! 也是未来插件 crate 的第一手消费示例)。

use std::borrow::Cow;
use std::sync::Arc;

use ek_core::PluginContext;
use ek_core::audit::{deploy_audit, shape_audit};
use ek_core::envelope::{Envelope, ReplyErr, e_req_type};
use ek_core::plugin::Plugin;
use ek_core::service::ServiceDesc;

/// 自用内测事件线(核心测试自承载;正式日志线约定件在 ek-data 侧,核心零依赖)。
pub const AUDIT_LOG: &str = "eAuditLog.";

pub struct AuditLogLine {
    pub name: String,
    pub msg: String,
}

pub struct TinyPlugin {
    name: String,
    ctx: std::sync::OnceLock<PluginContext>,
}

impl TinyPlugin {
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            ctx: std::sync::OnceLock::new(),
        }
    }
    fn put_log(&self, msg: &str) {
        if let Some(ctx) = self.ctx.get() {
            let _ = ctx.put(ek_event::Event::with_data(
                AUDIT_LOG,
                Arc::new(AuditLogLine {
                    name: self.name.clone(),
                    msg: msg.to_string(),
                }),
            ));
        }
    }
}

pub struct TinyReq;
pub struct TinyRes;

/// 服务面反例的 Req/Res 型(仅 讲 sentinel 用;零形态)。
pub struct Ping;
/// 服务面反例的 Res 型。
pub struct Pong;

impl Plugin for TinyPlugin {
    fn name(&self) -> &str {
        &self.name
    }

    fn routes(&self) -> Vec<Cow<'static, str>> {
        vec![
            Cow::Owned(e_req_type(&self.name)),
            Cow::Borrowed(ek_event::EVENT_TIMER),
        ]
    }

    fn version(&self) -> Cow<'static, str> {
        Cow::Borrowed(env!("CARGO_PKG_VERSION"))
    }

    fn services(&self) -> Vec<ServiceDesc> {
        vec![ServiceDesc::describe::<TinyReq, Result<TinyRes, ReplyErr>>(
            format!("{}.ping", self.name),
            "audit self-test ping (示例面;非通用服务)",
        )]
    }

    fn on_init(&self, ctx: &PluginContext) {
        let _ = self.ctx.set(ctx.clone());
    }

    fn on_start(&self) {
        self.put_log("on_start");
    }
    fn on_stop(&self) {
        self.put_log("on_stop");
    }

    fn on_mail(&self, event: &ek_event::Event) {
        if event.event_type == e_req_type(&self.name) {
            if let Some(env) = event.data.downcast_ref::<Envelope>() {
                if env.payload.downcast_ref::<TinyReq>().is_some() {
                    let res: TinyRes = TinyRes;
                    let _ = env.reply.send(Box::new(Ok::<TinyRes, ReplyErr>(res)));
                    return;
                }
                let _ = env
                    .reply
                    .send(Box::new(Err::<(), ReplyErr>(ReplyErr::InvalidPayload)));
            }
        }
    }
}

#[test]
fn tiny_plugin_passes_shape_and_deploy_audit() {
    let tiny = TinyPlugin::new("audit_tiny");
    let shape = shape_audit(&tiny, env!("CARGO_PKG_VERSION"));
    assert!(shape.is_ok(), "结构自检应全绿: {:?}", shape.findings);

    let dep = deploy_audit(Arc::new(TinyPlugin::new("audit_tiny_e2e")));
    assert!(dep.is_ok(), "装配自检应全绿: {:?}", dep.findings);
}

#[test]
fn audit_flags_missing_request_line_when_service_face_exists() {
    // 反例(0.4.3 语义):**有服务面**却漏报请求线 → Q2 必须抓出。
    struct BadRoutes;
    impl Plugin for BadRoutes {
        fn name(&self) -> &str {
            "audit_bad"
        }
        fn routes(&self) -> Vec<Cow<'static, str>> {
            vec![Cow::Borrowed(ek_event::EVENT_TIMER)]
        }
        fn version(&self) -> Cow<'static, str> {
            Cow::Borrowed(env!("CARGO_PKG_VERSION"))
        }
        fn services(&self) -> Vec<ServiceDesc> {
            vec![ServiceDesc::describe::<Ping, Pong>(
                "audit_bad.ping",
                "服务面反例",
            )]
        }
        fn on_mail(&self, _e: &ek_event::Event) {}
    }
    let rep = shape_audit(&BadRoutes, env!("CARGO_PKG_VERSION"));
    assert!(
        rep.findings.iter().any(|f| f.id == "Q2"),
        "Q2必须命中: {:?}",
        rep.findings
    );
}

#[test]
fn audit_passes_pure_subscriber_without_service_face() {
    // 正例(0.4.3 挂钩):**零服务面纯订阅件**(ui 一类)合法 —— 请求线自报与
    // D4 探针全部豁免;shape 双绿且**不产生任何 finding**。
    struct PureSubscriber;
    impl Plugin for PureSubscriber {
        fn name(&self) -> &str {
            "audit_pure_sub"
        }
        fn routes(&self) -> Vec<Cow<'static, str>> {
            vec![
                Cow::Borrowed(AUDIT_LOG),
                Cow::Borrowed(ek_event::EVENT_TIMER),
            ]
        }
        fn version(&self) -> Cow<'static, str> {
            Cow::Borrowed(env!("CARGO_PKG_VERSION"))
        }
        fn services(&self) -> Vec<ServiceDesc> {
            Vec::new()
        }
        fn on_mail(&self, _e: &ek_event::Event) {}
    }
    let shape = shape_audit(&PureSubscriber, env!("CARGO_PKG_VERSION"));
    assert!(shape.is_ok(), "纯订阅件应全绿: {:?}", shape.findings);
    let dep = deploy_audit(Arc::new(PureSubscriber));
    assert!(
        dep.is_ok(),
        "纯订阅件装配应全绿(免探针): {:?}",
        dep.findings
    );
}