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]
64pub fn shape_audit(plugin: &dyn Plugin) -> ShapeReport {
65 let mut rep = ShapeReport::default();
66 let name = plugin.name().to_string();
67
68 if plugin.version() != std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION")) {
70 rep.fail("Q1", "version() 必须显式覆写为本 crate 版本(七律 6)");
71 }
72
73 let routes: Vec<String> = plugin.routes().iter().map(|c| c.to_string()).collect();
75 if routes.is_empty() {
76 rep.fail("Q2", "routes() 为空 = 无订阅面(至少自报请求线)");
77 } else {
78 let mut seen = std::collections::BTreeSet::new();
79 for r in &routes {
80 if !seen.insert(r.clone()) {
81 rep.fail("Q2", format!("routes() 重复订阅 {r}"));
82 }
83 }
84 let req_line = e_req_type(&name);
85 if !routes.iter().any(|r| r == &req_line) {
86 rep.fail(
87 "Q2",
88 format!("routes() 必须自报请求线 {req_line};漏报 = 服务请求永远进不来"),
89 );
90 }
91 }
92
93 let svcs = plugin.services();
95 let mut ids = std::collections::BTreeSet::new();
96 for s in &svcs {
97 let id: &String = &s.id;
98 if !ids.insert(id.clone()) {
99 rep.fail("Q3", format!("服务目录重复 {id}"));
100 }
101 let expected = format!("{name}.");
102 if !id.starts_with(&expected) {
103 rep.fail("Q3", format!("服务全名未挂实例名前缀: {id}"));
104 }
105 }
106
107 for s in &svcs {
109 if s.doc.trim().is_empty() {
110 rep.fail("Q4", format!("服务 {} 描述为空", s.id));
111 }
112 }
113
114 if let Some(cfg) = plugin.as_configurable() {
116 if !valid_config_ns(cfg.config_ns()) {
117 rep.fail("Q5", format!("config_ns 非法: {}", cfg.config_ns()));
118 }
119 }
120
121 rep
122}
123
124struct AuditProbeReq;
126
127#[derive(Debug, Default)]
129pub struct DeployReport {
130 pub findings: Vec<Finding>,
132}
133
134impl DeployReport {
135 fn fail(&mut self, id: &'static str, text: impl Into<String>) {
136 self.findings.push(Finding {
137 id,
138 severity: Severity::Fail,
139 text: text.into(),
140 });
141 }
142
143 #[must_use]
145 pub fn is_ok(&self) -> bool {
146 self.findings.is_empty()
147 }
148}
149
150pub fn deploy_audit<T: Plugin + 'static>(plugin: Arc<T>) -> DeployReport {
156 let mut rep = DeployReport::default();
157 let name = plugin.name().to_string();
158 let ev = Arc::new(EventEngine::new());
159 let host = MainEngine::new(Some(ev.clone()));
160 if let Err(e) = host.add_plugin(plugin) {
161 rep.fail("D1", format!("add_plugin 失败: {e}"));
162 return rep;
163 }
164 if let Err(e) = host.start() {
165 rep.fail("D2", format!("host.start 失败: {e}"));
166 return rep;
167 }
168
169 if host.get_state(&name).is_none() {
171 rep.fail("D3", "实例未在册(装配失败)");
172 }
173
174 let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
176 let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
177 if let Err(e) = put {
178 rep.fail("D4", format!("总线投递失败: {e:?}"));
179 } else {
180 match rx.recv_timeout(Duration::from_secs(10)) {
181 Ok(boxed) => {
182 let typed = boxed.downcast::<Result<(), ReplyErr>>();
183 match typed {
184 Ok(inner) => {
185 if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
186 } else {
188 rep.fail(
189 "D4",
190 "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
191 );
192 }
193 }
194 Err(_) => {
195 }
197 }
198 }
199 Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
200 }
201 }
202
203 host.close(Duration::from_secs(2));
205 rep
206}