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 let mut rep = DeployReport::default();
185 let name = plugin.name().to_string();
186 let has_service_face = !plugin.services().is_empty();
187 let ev = Arc::new(EventEngine::new());
188 let host = MainEngine::new(Some(ev.clone()));
189 if let Err(e) = host.add_plugin(plugin) {
190 rep.fail("D1", format!("add_plugin 失败: {e}"));
191 return rep;
192 }
193 if let Err(e) = host.start() {
194 rep.fail("D2", format!("host.start 失败: {e}"));
195 return rep;
196 }
197
198 if host.get_state(&name).is_none() {
200 rep.fail("D3", "实例未在册(装配失败)");
201 }
202
203 if has_service_face {
205 let (env, rx) = Envelope::new(Arc::new(AuditProbeReq) as Arc<dyn Any + Send + Sync>);
206 let put = ev.put(Event::with_data(e_req_type(&name), Arc::new(env)));
207 if let Err(e) = put {
208 rep.fail("D4", format!("总线投递失败: {e:?}"));
209 } else {
210 match rx.recv_timeout(Duration::from_secs(10)) {
211 Ok(boxed) => {
212 let typed = boxed.downcast::<Result<(), ReplyErr>>();
213 match typed {
214 Ok(inner) => {
215 if matches!(*inner, Err(ReplyErr::InvalidPayload)) {
216 } else {
218 rep.fail(
219 "D4",
220 "探针未注册载荷竟然回了非 InvalidPayload 形态(契约应答面失真)",
221 );
222 }
223 }
224 Err(_) => {
225 }
227 }
228 }
229 Err(_) => rep.fail("D4", "请求探针超时:请求线未自报(routes 漏报)或分派断"),
230 }
231 }
232 }
234 host.close(Duration::from_secs(2));
236 rep
237}