1use crate::envelope::{Envelope, ReplyErr, e_req_type};
10use crate::main_engine::MainEngine;
11use crate::plugin::Plugin;
12use crate::section::valid_config_ns;
13use ek_event::{Event, EventEngine};
14use std::any::Any;
15use std::sync::Arc;
16use std::time::Duration;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum Severity {
21 Fail,
23}
24
25#[derive(Debug, Clone)]
27pub struct Finding {
28 pub id: &'static str,
30 pub severity: Severity,
32 pub text: String,
34}
35
36#[derive(Debug, Default)]
38pub struct ShapeReport {
39 pub findings: Vec<Finding>,
41}
42
43impl ShapeReport {
44 fn fail(&mut self, id: &'static str, text: impl Into<String>) {
45 self.findings.push(Finding {
46 id,
47 severity: Severity::Fail,
48 text: text.into(),
49 });
50 }
51
52 #[must_use]
54 pub fn is_ok(&self) -> bool {
55 self.findings.is_empty()
56 }
57}
58
59#[must_use]
68pub fn shape_audit(plugin: &dyn Plugin, expected_version: &str) -> ShapeReport {
69 let mut rep = ShapeReport::default();
70 let name = plugin.name().to_string();
71
72 if plugin.version().as_ref() != expected_version {
74 rep.fail(
75 "Q1",
76 format!(
77 "version() 必须显式覆写为本 crate 版本(七律 6);期望 {expected_version},实得 {}",
78 plugin.version()
79 ),
80 );
81 }
82
83 let routes: Vec<String> = plugin.routes().iter().map(|c| c.to_string()).collect();
85 if routes.is_empty() {
86 rep.fail("Q2", "routes() 为空 = 无订阅面(至少自报请求线)");
87 } else {
88 let mut seen = std::collections::BTreeSet::new();
89 for r in &routes {
90 if !seen.insert(r.clone()) {
91 rep.fail("Q2", format!("routes() 重复订阅 {r}"));
92 }
93 }
94 let req_line = e_req_type(&name);
95 if !routes.iter().any(|r| r == &req_line) {
96 rep.fail(
97 "Q2",
98 format!("routes() 必须自报请求线 {req_line};漏报 = 服务请求永远进不来"),
99 );
100 }
101 }
102
103 let svcs = plugin.services();
105 let mut ids = std::collections::BTreeSet::new();
106 for s in &svcs {
107 let id: &String = &s.id;
108 if !ids.insert(id.clone()) {
109 rep.fail("Q3", format!("服务目录重复 {id}"));
110 }
111 let expected = format!("{name}.");
112 if !id.starts_with(&expected) {
113 rep.fail("Q3", format!("服务全名未挂实例名前缀: {id}"));
114 }
115 }
116
117 for s in &svcs {
119 if s.doc.trim().is_empty() {
120 rep.fail("Q4", format!("服务 {} 描述为空", s.id));
121 }
122 }
123
124 if let Some(cfg) = plugin.as_configurable() {
126 if !valid_config_ns(cfg.config_ns()) {
127 rep.fail("Q5", format!("config_ns 非法: {}", cfg.config_ns()));
128 }
129 }
130
131 rep
132}
133
134struct AuditProbeReq;
136
137#[derive(Debug, Default)]
139pub struct DeployReport {
140 pub findings: Vec<Finding>,
142}
143
144impl DeployReport {
145 fn fail(&mut self, id: &'static str, text: impl Into<String>) {
146 self.findings.push(Finding {
147 id,
148 severity: Severity::Fail,
149 text: text.into(),
150 });
151 }
152
153 #[must_use]
155 pub fn is_ok(&self) -> bool {
156 self.findings.is_empty()
157 }
158}
159
160pub fn deploy_audit<T: Plugin + 'static>(plugin: Arc<T>) -> DeployReport {
166 let mut rep = DeployReport::default();
167 let name = plugin.name().to_string();
168 let ev = Arc::new(EventEngine::new());
169 let host = MainEngine::new(Some(ev.clone()));
170 if let Err(e) = host.add_plugin(plugin) {
171 rep.fail("D1", format!("add_plugin 失败: {e}"));
172 return rep;
173 }
174 if let Err(e) = host.start() {
175 rep.fail("D2", format!("host.start 失败: {e}"));
176 return rep;
177 }
178
179 if host.get_state(&name).is_none() {
181 rep.fail("D3", "实例未在册(装配失败)");
182 }
183
184 let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
186 let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
187 if let Err(e) = put {
188 rep.fail("D4", format!("总线投递失败: {e:?}"));
189 } else {
190 match rx.recv_timeout(Duration::from_secs(10)) {
191 Ok(boxed) => {
192 let typed = boxed.downcast::<Result<(), ReplyErr>>();
193 match typed {
194 Ok(inner) => {
195 if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
196 } else {
198 rep.fail(
199 "D4",
200 "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
201 );
202 }
203 }
204 Err(_) => {
205 }
207 }
208 }
209 Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
210 }
211 }
212
213 host.close(Duration::from_secs(2));
215 rep
216}