nichlink/registry_core/diagnostic/
build.rs1use super::*;
2
3use crate::json::push_json_string;
8
9#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct BuildDiagnostic {
18 pub phase: &'static str,
22 pub branch: String,
25 pub node: String,
29 pub source: String,
32 pub line: usize,
35 pub function: String,
39 pub field: String,
42 pub expected: String,
45 pub actual: String,
48 pub provider: String,
52 pub message: String,
55}
56
57impl BuildDiagnostic {
58 pub fn new(phase: &'static str, message: impl Into<String>) -> Self {
65 Self {
66 phase,
67 branch: String::new(),
68 node: String::new(),
69 source: String::new(),
70 line: 0,
71 function: String::new(),
72 field: String::new(),
73 expected: String::new(),
74 actual: String::new(),
75 provider: String::new(),
76 message: message.into(),
77 }
78 }
79
80 pub fn at(mut self, source: impl Into<String>, line: usize) -> Self {
83 self.source = source.into();
84 self.line = line;
85 self
86 }
87
88 pub fn node(mut self, node: impl Into<String>, kind: impl Into<String>) -> Self {
91 self.node = format!("{} ({})", node.into(), kind.into());
92 self
93 }
94
95 pub fn branch(mut self, branch: impl Into<String>) -> Self {
98 self.branch = branch.into();
99 self
100 }
101
102 pub fn function(mut self, function: impl Into<String>) -> Self {
105 self.function = function.into();
106 self
107 }
108
109 pub fn field(mut self, field: impl Into<String>) -> Self {
112 self.field = field.into();
113 self
114 }
115
116 pub fn expected(mut self, expected: impl Into<String>) -> Self {
119 self.expected = expected.into();
120 self
121 }
122
123 pub fn actual(mut self, actual: impl Into<String>) -> Self {
126 self.actual = actual.into();
127 self
128 }
129
130 pub fn provider(mut self, provider: impl Into<String>) -> Self {
133 self.provider = provider.into();
134 self
135 }
136}
137
138#[derive(Clone, Debug, Default, Eq, PartialEq)]
141pub struct BuildDiagnostics {
142 items: Vec<BuildDiagnostic>,
143}
144
145impl BuildDiagnostics {
146 pub fn push(&mut self, diagnostic: BuildDiagnostic) {
151 self.items.push(diagnostic);
152 }
153
154 pub fn extend(&mut self, other: Self) {
157 self.items.extend(other.items);
158 }
159
160 pub fn is_empty(&self) -> bool {
163 self.items.is_empty()
164 }
165
166 pub fn iter(&self) -> impl Iterator<Item = &BuildDiagnostic> {
181 let mut items = self.items.iter().collect::<Vec<_>>();
182 items.sort_by(|left, right| {
183 (
184 left.phase,
185 &left.source,
186 left.line,
187 &left.node,
188 &left.message,
189 )
190 .cmp(&(
191 right.phase,
192 &right.source,
193 right.line,
194 &right.node,
195 &right.message,
196 ))
197 });
198 items.dedup();
199 items.into_iter()
200 }
201
202 pub fn len(&self) -> usize {
206 self.iter().count()
207 }
208
209 pub fn render(&self) -> String {
213 if self.items.is_empty() {
214 return String::new();
215 }
216 let mut output = String::from("NICHLink BUILD CHECK FAILED / NichLink 构建检查失败\n");
217 for (index, diagnostic) in self.iter().enumerate() {
218 if index > 0 {
219 output.push('\n');
220 }
221 render_build_item(&mut output, diagnostic);
222 }
223 output
224 }
225
226 pub fn to_json(&self) -> String {
247 let mut output = String::from("{\"schema\":\"nichlink.build-diagnostics/1\",\"count\":");
248 output.push_str(&self.len().to_string());
249 output.push_str(",\"diagnostics\":[");
250 for (index, diagnostic) in self.iter().enumerate() {
251 if index > 0 {
252 output.push(',');
253 }
254 output.push_str("{\"phase\":");
255 push_json_string(&mut output, diagnostic.phase);
256 output.push_str(",\"branch\":");
257 push_json_string(&mut output, &diagnostic.branch);
258 output.push_str(",\"node\":");
259 push_json_string(&mut output, &diagnostic.node);
260 output.push_str(",\"source\":");
261 push_json_string(&mut output, &diagnostic.source);
262 output.push_str(",\"line\":");
263 output.push_str(&diagnostic.line.to_string());
264 output.push_str(",\"function\":");
265 push_json_string(&mut output, &diagnostic.function);
266 output.push_str(",\"field\":");
267 push_json_string(&mut output, &diagnostic.field);
268 output.push_str(",\"expected\":");
269 push_json_string(&mut output, &diagnostic.expected);
270 output.push_str(",\"actual\":");
271 push_json_string(&mut output, &diagnostic.actual);
272 output.push_str(",\"provider\":");
273 push_json_string(&mut output, &diagnostic.provider);
274 output.push_str(",\"message\":");
275 push_json_string(&mut output, &diagnostic.message);
276 output.push('}');
277 }
278 output.push_str("]}");
279 output
280 }
281}
282
283fn render_build_item(output: &mut String, diagnostic: &BuildDiagnostic) {
284 let phase = match diagnostic.phase {
285 "requirements" => "requirements / 注册需求",
286 "contract" => "contract / 注册合同",
287 "stable-identity" => "stable identity / 稳定标识",
288 "static-plan" => "static plan / 静态计划",
289 other => other,
290 };
291 writeln!(
292 output,
293 "+-- phase={phase} branch={}",
294 unknown(&diagnostic.branch)
295 )
296 .unwrap();
297 if !diagnostic.node.is_empty() {
298 writeln!(output, "| node={}", diagnostic.node).unwrap();
299 }
300 if !diagnostic.source.is_empty() {
301 if diagnostic.line == 0 {
302 writeln!(output, "| source={}", diagnostic.source).unwrap();
303 } else {
304 writeln!(
305 output,
306 "| source={}:{}",
307 diagnostic.source, diagnostic.line
308 )
309 .unwrap();
310 }
311 }
312 if !diagnostic.function.is_empty() {
313 writeln!(output, "| function={}", diagnostic.function).unwrap();
314 }
315 if !diagnostic.field.is_empty() {
316 writeln!(output, "| field={}", diagnostic.field).unwrap();
317 }
318 if !diagnostic.expected.is_empty() {
319 writeln!(output, "| expected={}", diagnostic.expected).unwrap();
320 }
321 if !diagnostic.actual.is_empty() {
322 writeln!(output, "| actual={}", diagnostic.actual).unwrap();
323 }
324 if !diagnostic.provider.is_empty() {
325 writeln!(output, "| provider={}", diagnostic.provider).unwrap();
326 }
327 writeln!(output, "`-- {}", diagnostic.message).unwrap();
328}
329
330fn unknown(value: &str) -> &str {
331 if value.is_empty() { "<unknown>" } else { value }
332}
333
334#[cfg(test)]
335mod build_diagnostic_tests {
336 use super::{BuildDiagnostic, BuildDiagnostics};
337
338 #[test]
339 fn render_keeps_source_and_contract_fields_together() {
340 let mut diagnostics = BuildDiagnostics::default();
341 let diagnostic = BuildDiagnostic::new("contract", "output contract does not match")
342 .branch("control")
343 .node("abc", "Button")
344 .at("control/button.rs", 12)
345 .function("Button::render")
346 .field("output")
347 .expected("Canvas")
348 .actual("String")
349 .provider("CanvasProvider");
350 diagnostics.push(diagnostic.clone());
351 diagnostics.push(diagnostic);
352 let rendered = diagnostics.render();
353 assert!(rendered.contains("phase=contract / 注册合同"));
354 assert!(rendered.contains("source=control/button.rs:12"));
355 assert!(rendered.contains("expected=Canvas"));
356 assert_eq!(
357 rendered.matches("output contract does not match").count(),
358 1
359 );
360 }
361
362 #[test]
368 fn iteration_len_and_json_agree_with_the_rendered_set() {
369 let mut diagnostics = BuildDiagnostics::default();
370 assert!(diagnostics.is_empty());
371 assert_eq!(diagnostics.len(), 0);
372 assert_eq!(diagnostics.iter().count(), 0);
373 assert_eq!(
374 diagnostics.to_json(),
375 "{\"schema\":\"nichlink.build-diagnostics/1\",\"count\":0,\"diagnostics\":[]}"
376 );
377
378 diagnostics.push(BuildDiagnostic::new("contract", "second message"));
379 diagnostics.push(BuildDiagnostic::new("requirements", "first message"));
380 diagnostics.push(BuildDiagnostic::new("requirements", "first message"));
381
382 assert!(!diagnostics.is_empty());
383 assert_eq!(diagnostics.len(), 2);
384 assert_eq!(diagnostics.iter().count(), 2);
385 let rendered = diagnostics.render();
386 assert_eq!(rendered.matches("first message").count(), 1);
387 assert_eq!(rendered.matches("second message").count(), 1);
388
389 let json = diagnostics.to_json();
390 assert!(json.starts_with("{\"schema\":\"nichlink.build-diagnostics/1\",\"count\":2,"));
391 assert!(json.ends_with("]}"));
392 assert_eq!(json.matches("first message").count(), 1);
393 assert_eq!(json.matches("second message").count(), 1);
394 let first = json.find("second message").expect("contract diagnostic");
400 let second = json.find("first message").expect("requirements diagnostic");
401 assert!(first < second, "{json}");
402 }
403
404 #[test]
410 fn json_escapes_quotes_backslashes_and_control_characters() {
411 let mut diagnostics = BuildDiagnostics::default();
412 diagnostics.push(
413 BuildDiagnostic::new("contract", "expected \"Canvas\" \\ actual\nnext\ttab")
414 .at("control/button.rs", 3),
415 );
416 let json = diagnostics.to_json();
417 assert!(json.contains(r#"\"Canvas\""#), "{json}");
418 assert!(json.contains(r"\\ actual"), "{json}");
419 assert!(json.contains(r"\nnext\ttab"), "{json}");
420 assert!(
421 !json.contains('\n') && !json.contains('\t'),
422 "a control character must never reach the document raw: {json}"
423 );
424 assert!(
425 json.contains("\"source\":\"control/button.rs\",\"line\":3"),
426 "{json}"
427 );
428 }
429}