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]
76pub fn shape_audit(plugin: &dyn Plugin, expected_version: &str) -> ShapeReport {
77 let mut rep = ShapeReport::default();
78 let name = plugin.name().to_string();
79
80 if plugin.version().as_ref() != expected_version {
82 rep.fail(
83 "Q1",
84 format!(
85 "version() 必须显式覆写为本 crate 版本(七律 6);期望 {expected_version},实得 {}",
86 plugin.version()
87 ),
88 );
89 }
90
91 let svcs = plugin.services();
97 let routes: Vec<String> = plugin.routes().iter().map(|c| c.to_string()).collect();
98 if routes.is_empty() {
99 rep.fail("Q2", "routes() 为空 = 无订阅面(至少自报请求线)");
100 } else {
101 let mut seen = std::collections::BTreeSet::new();
102 for r in &routes {
103 if !seen.insert(r.clone()) {
104 rep.fail("Q2", format!("routes() 重复订阅 {r}"));
105 }
106 }
107 if !svcs.is_empty() {
108 let req_line = e_req_type(&name);
109 if !routes.iter().any(|r| r == &req_line) {
110 rep.fail(
111 "Q2",
112 format!("routes() 必须自报请求线 {req_line};漏报 = 服务请求永远进不来"),
113 );
114 }
115 }
116 }
117
118 let mut ids = std::collections::BTreeSet::new();
120 for s in &svcs {
121 let id: &String = &s.id;
122 if !ids.insert(id.clone()) {
123 rep.fail("Q3", format!("服务目录重复 {id}"));
124 }
125 let expected = format!("{name}.");
126 if !id.starts_with(&expected) {
127 rep.fail("Q3", format!("服务全名未挂实例名前缀: {id}"));
128 }
129 }
130
131 for s in &svcs {
133 if s.doc.trim().is_empty() {
134 rep.fail("Q4", format!("服务 {} 描述为空", s.id));
135 }
136 }
137
138 if let Some(cfg) = plugin.as_configurable() {
140 if !valid_config_ns(cfg.config_ns()) {
141 rep.fail("Q5", format!("config_ns 非法: {}", cfg.config_ns()));
142 }
143 }
144
145 rep
146}
147
148struct AuditProbeReq;
150
151#[derive(Debug, Default)]
153pub struct DeployReport {
154 pub findings: Vec<Finding>,
156}
157
158impl DeployReport {
159 fn fail(&mut self, id: &'static str, text: impl Into<String>) {
160 self.findings.push(Finding {
161 id,
162 severity: Severity::Fail,
163 text: text.into(),
164 });
165 }
166
167 #[must_use]
169 pub fn is_ok(&self) -> bool {
170 self.findings.is_empty()
171 }
172}
173
174pub fn deploy_audit<T: Plugin + 'static>(plugin: Arc<T>) -> DeployReport {
184 deploy_audit_with_peers(plugin, Vec::new())
185}
186
187struct PeerWrap(Arc<dyn Plugin>);
190
191impl Plugin for PeerWrap {
192 fn name(&self) -> &str {
193 self.0.name()
194 }
195 fn deps(&self) -> &[&str] {
196 self.0.deps()
197 }
198 fn routes(&self) -> Vec<std::borrow::Cow<'static, str>> {
199 self.0.routes()
200 }
201 fn mailbox_soft_cap(&self) -> u64 {
202 self.0.mailbox_soft_cap()
203 }
204 fn version(&self) -> std::borrow::Cow<'static, str> {
205 self.0.version()
206 }
207 fn as_any(&self) -> Option<&dyn std::any::Any> {
208 self.0.as_any()
209 }
210 fn as_configurable(&self) -> Option<&dyn crate::section::Configurable> {
211 self.0.as_configurable()
212 }
213 fn services(&self) -> Vec<crate::service::ServiceDesc> {
214 self.0.services()
215 }
216 fn on_init(&self, ctx: &crate::context::PluginContext) {
217 self.0.on_init(ctx);
218 }
219 fn on_start(&self) {
220 self.0.on_start();
221 }
222 fn on_stop(&self) {
223 self.0.on_stop();
224 }
225 fn on_mail(&self, event: &ek_event::Event) {
226 self.0.on_mail(event);
227 }
228}
229
230pub fn deploy_audit_with_peers<T: Plugin + 'static>(
236 plugin: Arc<T>,
237 peers: Vec<Arc<dyn Plugin>>,
238) -> DeployReport {
239 let mut rep = DeployReport::default();
240 let name = plugin.name().to_string();
241 let has_service_face = !plugin.services().is_empty();
242 let ev = Arc::new(EventEngine::new());
243 let host = MainEngine::new(Some(ev.clone()));
244 for p in peers {
245 if let Err(e) = host.add_plugin(Arc::new(PeerWrap(p))) {
246 rep.fail("D1", format!("同伴装配失败: {e}"));
247 return rep;
248 }
249 }
250 if let Err(e) = host.add_plugin(plugin) {
251 rep.fail("D1", format!("add_plugin 失败: {e}"));
252 return rep;
253 }
254 if let Err(e) = host.start() {
255 rep.fail("D2", format!("host.start 失败: {e}"));
256 return rep;
257 }
258
259 if host.get_state(&name).is_none() {
261 rep.fail("D3", "实例未在册(装配失败)");
262 }
263
264 if has_service_face {
266 let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
267 let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
268 if let Err(e) = put {
269 rep.fail("D4", format!("总线投递失败: {e:?}"));
270 } else {
271 match rx.recv_timeout(Duration::from_secs(10)) {
272 Ok(boxed) => {
273 let typed = boxed.downcast::<Result<(), ReplyErr>>();
274 match typed {
275 Ok(inner) => {
276 if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
277 } else {
279 rep.fail(
280 "D4",
281 "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
282 );
283 }
284 }
285 Err(_) => {
286 }
288 }
289 }
290 Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
291 }
292 }
293 }
295 host.close(Duration::from_secs(2));
297 rep
298}