1use std::collections::{BTreeMap, BTreeSet, btree_map::Entry};
2use std::fmt;
3
4use serde_json::{Map, Value};
5
6use crate::Error;
7use crate::records::{DiagnosticId, SourceSpan};
8use crate::validation::{MAX_DIAGNOSTIC_CODE_BYTES, sanitize_message, valid_nonempty_text};
9
10fn record_too_large(what: &str, limit: usize) -> Error {
11 Error::new(
12 &crate::codes::REQUEST_RECORD_TOO_LARGE,
13 format!("a diagnostic carries more than {limit} {what}"),
14 )
15}
16
17fn check_details(details: &Map<String, Value>) -> Result<(), Error> {
18 if details.len() > crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS {
19 return Err(record_too_large(
20 "detail keys",
21 crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS,
22 ));
23 }
24 if let Some((key, _)) = details
25 .iter()
26 .find(|(key, _)| !crate::validation::valid_nonempty_text(key))
27 {
28 return Err(Error::new(
29 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
30 format!(
31 "diagnostic detail key `{}` is empty, contains NUL, or exceeds its bound",
32 bounded_identifier(key)
33 ),
34 ));
35 }
36 Ok(())
37}
38
39#[derive(
44 Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
45)]
46#[serde(rename_all = "snake_case")]
47pub enum DiagnosticSeverity {
48 Note,
50 Remark,
52 Warning,
54 Error,
56}
57
58impl DiagnosticSeverity {
59 #[must_use]
61 pub const fn as_str(self) -> &'static str {
62 match self {
63 Self::Note => "note",
64 Self::Remark => "remark",
65 Self::Warning => "warning",
66 Self::Error => "error",
67 }
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73pub enum ErrorCategory {
74 Io,
75 Request,
76 Parse,
77 Data,
78 Output,
79}
80
81impl ErrorCategory {
82 pub const ALL: [Self; 5] = [
83 Self::Io,
84 Self::Request,
85 Self::Parse,
86 Self::Data,
87 Self::Output,
88 ];
89
90 pub const TOKENS: [&'static str; 5] = ["io", "request", "parse", "data", "output"];
91
92 #[must_use]
93 pub const fn as_str(self) -> &'static str {
94 match self {
95 Self::Io => "io",
96 Self::Request => "request",
97 Self::Parse => "parse",
98 Self::Data => "data",
99 Self::Output => "output",
100 }
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
106#[non_exhaustive]
107pub enum DiagnosticStage {
108 Parse,
109 Read,
110 Canonicalize,
111 Validate,
112 Transform,
113 Build,
114 Emit,
115 Bind,
116 Partner,
117 Request,
118}
119
120impl DiagnosticStage {
121 pub const ALL: &'static [Self] = &[
122 Self::Parse,
123 Self::Read,
124 Self::Canonicalize,
125 Self::Validate,
126 Self::Transform,
127 Self::Build,
128 Self::Emit,
129 Self::Bind,
130 Self::Partner,
131 Self::Request,
132 ];
133
134 pub const NAMESPACES: &'static [&'static str] = &[
135 "PARSE",
136 "READ",
137 "CANONICALIZE",
138 "VALIDATE",
139 "TRANSFORM",
140 "BUILD",
141 "EMIT",
142 "BIND",
143 "PARTNER",
144 "REQUEST",
145 ];
146
147 #[must_use]
148 pub const fn namespace(self) -> &'static str {
149 match self {
150 Self::Parse => "PARSE",
151 Self::Read => "READ",
152 Self::Canonicalize => "CANONICALIZE",
153 Self::Validate => "VALIDATE",
154 Self::Transform => "TRANSFORM",
155 Self::Build => "BUILD",
156 Self::Emit => "EMIT",
157 Self::Bind => "BIND",
158 Self::Partner => "PARTNER",
159 Self::Request => "REQUEST",
160 }
161 }
162
163 #[must_use]
164 pub fn from_namespace(namespace: &str) -> Option<Self> {
165 Self::ALL
166 .iter()
167 .copied()
168 .find(|stage| stage.namespace() == namespace)
169 }
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
174pub struct DiagnosticCode(Box<str>);
175
176impl DiagnosticCode {
177 pub fn new(code: impl Into<String>) -> Result<Self, crate::Error> {
179 let code = code.into();
180 if !code_is_well_formed(&code) || code.len() > MAX_DIAGNOSTIC_CODE_BYTES {
181 return Err(crate::Error::new(
182 &crate::codes::REQUEST_DIAGNOSTIC_INVALID_CODE,
183 format!("invalid diagnostic code `{}`", bounded_identifier(&code)),
184 ));
185 }
186 Ok(Self(code.into_boxed_str()))
187 }
188
189 #[must_use]
190 pub fn as_str(&self) -> &str {
191 &self.0
192 }
193
194 #[must_use]
195 pub fn namespace(&self) -> &str {
196 self.0.split('.').next().unwrap_or("")
197 }
198
199 #[must_use]
200 pub fn stage(&self) -> Option<DiagnosticStage> {
201 DiagnosticStage::from_namespace(self.namespace())
202 }
203}
204
205impl fmt::Display for DiagnosticCode {
206 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
207 formatter.write_str(&self.0)
208 }
209}
210
211#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum CodeStatus {
214 Active,
215 Retired { since: &'static str },
216}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub struct DiagnosticInfo {
221 pub code: &'static str,
222 pub severity: DiagnosticSeverity,
223 pub category: Option<ErrorCategory>,
224 pub summary: &'static str,
225 pub status: CodeStatus,
226}
227
228impl DiagnosticInfo {
229 #[must_use]
230 pub const fn new(
231 code: &'static str,
232 severity: DiagnosticSeverity,
233 summary: &'static str,
234 ) -> Self {
235 Self {
236 code,
237 severity,
238 category: None,
239 summary,
240 status: CodeStatus::Active,
241 }
242 }
243
244 #[must_use]
245 pub const fn with_category(mut self, category: ErrorCategory) -> Self {
246 self.category = Some(category);
247 self
248 }
249
250 #[must_use]
251 pub const fn retired(mut self, since: &'static str) -> Self {
252 self.status = CodeStatus::Retired { since };
253 self
254 }
255
256 #[must_use]
257 pub fn namespace(&self) -> &'static str {
258 self.code.split('.').next().unwrap_or("")
259 }
260
261 #[must_use]
262 pub fn stage(&self) -> Option<DiagnosticStage> {
263 DiagnosticStage::from_namespace(self.namespace())
264 }
265}
266
267#[derive(Clone, Debug)]
268enum DiagnosticIdentity {
269 Registered(&'static DiagnosticInfo),
270 External(DiagnosticCode),
271}
272
273#[derive(Clone, Debug)]
275pub struct Diagnostic {
276 identity: DiagnosticIdentity,
277 id: Option<DiagnosticId>,
278 severity: DiagnosticSeverity,
279 message: String,
280 target: Option<String>,
281 spans: Vec<SourceSpan>,
282 related: Vec<DiagnosticId>,
283 details: Map<String, Value>,
284 suggested_action: Option<String>,
285}
286
287impl Diagnostic {
288 #[must_use]
290 pub fn new(
291 code: DiagnosticCode,
292 severity: DiagnosticSeverity,
293 message: impl Into<String>,
294 ) -> Self {
295 Self {
296 identity: DiagnosticIdentity::External(code),
297 id: None,
298 severity,
299 message: sanitize_message(message),
300 target: None,
301 spans: Vec::new(),
302 related: Vec::new(),
303 details: Map::new(),
304 suggested_action: None,
305 }
306 }
307
308 #[must_use]
310 pub fn of(info: &'static DiagnosticInfo, message: impl Into<String>) -> Self {
311 Self {
312 identity: DiagnosticIdentity::Registered(info),
313 id: None,
314 severity: info.severity,
315 message: sanitize_message(message),
316 target: None,
317 spans: Vec::new(),
318 related: Vec::new(),
319 details: Map::new(),
320 suggested_action: None,
321 }
322 }
323
324 #[must_use]
325 pub fn code(&self) -> &str {
326 match &self.identity {
327 DiagnosticIdentity::Registered(info) => info.code,
328 DiagnosticIdentity::External(code) => code.as_str(),
329 }
330 }
331
332 #[must_use]
333 pub fn registered_info(&self) -> Option<&'static DiagnosticInfo> {
334 match self.identity {
335 DiagnosticIdentity::Registered(info) => Some(info),
336 DiagnosticIdentity::External(_) => None,
337 }
338 }
339
340 #[must_use]
341 pub fn stage(&self) -> Option<DiagnosticStage> {
342 DiagnosticStage::from_namespace(self.code().split('.').next().unwrap_or(""))
343 }
344
345 #[must_use]
346 pub const fn id(&self) -> Option<&DiagnosticId> {
347 self.id.as_ref()
348 }
349
350 #[must_use]
351 pub const fn severity(&self) -> DiagnosticSeverity {
352 self.severity
353 }
354
355 #[must_use]
356 pub fn message(&self) -> &str {
357 &self.message
358 }
359
360 #[must_use]
361 pub fn target(&self) -> Option<&str> {
362 self.target.as_deref()
363 }
364
365 #[must_use]
366 pub fn spans(&self) -> &[SourceSpan] {
367 &self.spans
368 }
369
370 #[must_use]
371 pub fn related(&self) -> &[DiagnosticId] {
372 &self.related
373 }
374
375 #[must_use]
376 pub const fn details(&self) -> &Map<String, Value> {
377 &self.details
378 }
379
380 #[must_use]
381 pub fn with_id(mut self, id: DiagnosticId) -> Self {
382 self.id = Some(id);
383 self
384 }
385
386 #[must_use]
389 pub fn with_code(mut self, info: &'static DiagnosticInfo) -> Self {
390 self.identity = DiagnosticIdentity::Registered(info);
391 self
392 }
393
394 #[must_use]
395 pub fn with_severity(mut self, severity: DiagnosticSeverity) -> Self {
396 self.severity = severity;
397 self
398 }
399
400 pub fn with_target(mut self, target: impl Into<String>) -> Result<Self, Error> {
407 self.set_target(target)?;
408 Ok(self)
409 }
410
411 #[must_use]
413 pub fn target_is_pointer(&self) -> bool {
414 self.target
415 .as_deref()
416 .is_some_and(crate::validation::valid_rfc6901_pointer)
417 }
418
419 pub fn with_span(mut self, span: SourceSpan) -> Result<Self, Error> {
420 self.add_span(span)?;
421 Ok(self)
422 }
423
424 pub(crate) fn add_span(&mut self, span: SourceSpan) -> Result<(), Error> {
427 if self.spans.len() >= crate::validation::MAX_DIAGNOSTIC_SPANS {
428 return Err(record_too_large(
429 "source spans",
430 crate::validation::MAX_DIAGNOSTIC_SPANS,
431 ));
432 }
433 self.spans.push(span);
434 Ok(())
435 }
436
437 pub(crate) fn prefix_target(&mut self, prefix: &str) -> Result<(), Error> {
438 let Some(target) = self.target.take() else {
439 return Ok(());
440 };
441 self.set_target(format!("{prefix}{target}"))
442 }
443
444 pub(crate) fn remap_span_sources(
445 &mut self,
446 mut remap: impl FnMut(&crate::SourceId) -> crate::SourceId,
447 ) -> Result<(), Error> {
448 let mut spans = Vec::with_capacity(self.spans.len());
449 for span in &self.spans {
450 spans.push(SourceSpan::new(
451 remap(span.source()),
452 span.byte_start(),
453 span.byte_end(),
454 )?);
455 }
456 self.spans = spans;
457 Ok(())
458 }
459
460 pub fn with_related(mut self, related: DiagnosticId) -> Result<Self, Error> {
461 if self.related.len() >= crate::validation::MAX_DIAGNOSTIC_RELATED {
462 return Err(record_too_large(
463 "related records",
464 crate::validation::MAX_DIAGNOSTIC_RELATED,
465 ));
466 }
467 self.related.push(related);
468 Ok(self)
469 }
470
471 pub fn insert_detail(&mut self, key: impl Into<String>, value: Value) -> Result<(), Error> {
474 let key = key.into();
475 if !crate::validation::valid_nonempty_text(&key) {
476 return Err(Error::new(
477 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
478 "a diagnostic detail key must be nonempty and bounded",
479 ));
480 }
481 if !self.details.contains_key(&key)
482 && self.details.len() >= crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS
483 {
484 return Err(record_too_large(
485 "detail keys",
486 crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS,
487 ));
488 }
489 self.details.insert(key, value);
490 Ok(())
491 }
492
493 pub fn clear_target(&mut self) {
497 self.target = None;
498 }
499
500 pub fn set_target(&mut self, target: impl Into<String>) -> Result<(), Error> {
504 let target = target.into();
505 if !crate::validation::valid_diagnostic_target(&target) {
506 return Err(Error::new(
507 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
508 "a diagnostic target must be a nonempty bounded locator",
509 ));
510 }
511 self.target = Some(target);
512 Ok(())
513 }
514
515 pub fn with_details(mut self, details: Map<String, Value>) -> Result<Self, Error> {
516 self.set_details(details)?;
517 Ok(self)
518 }
519
520 pub fn set_details(&mut self, details: Map<String, Value>) -> Result<(), Error> {
523 check_details(&details)?;
524 self.details = details;
525 Ok(())
526 }
527
528 #[must_use]
530 pub fn suggested_action(&self) -> Option<&str> {
531 self.suggested_action.as_deref()
532 }
533
534 #[must_use]
535 pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
536 self.suggested_action = Some(sanitize_message(action));
537 self
538 }
539}
540
541impl PartialEq for Diagnostic {
542 fn eq(&self, other: &Self) -> bool {
543 self.code() == other.code()
544 && self.id == other.id
545 && self.severity == other.severity
546 && self.message == other.message
547 && self.target == other.target
548 && self.spans == other.spans
549 && self.related == other.related
550 && self.details == other.details
551 && self.suggested_action == other.suggested_action
552 }
553}
554
555pub fn check_registry<'a, I>(entries: I) -> Vec<String>
557where
558 I: IntoIterator<Item = &'a DiagnosticInfo>,
559{
560 let mut problems = Vec::new();
561 let mut seen = BTreeSet::new();
562 for entry in entries {
563 let retired = matches!(entry.status, CodeStatus::Retired { .. });
564 if !retired
565 && (!code_is_well_formed(entry.code) || entry.code.len() > MAX_DIAGNOSTIC_CODE_BYTES)
566 {
567 problems.push(format!("{}: does not match the code grammar", entry.code));
568 } else if !retired && entry.stage().is_none() {
569 problems.push(format!(
570 "{}: namespace {} is not one PowerIO emits",
571 entry.code,
572 entry.namespace()
573 ));
574 }
575 if !valid_nonempty_text(entry.summary) {
576 problems.push(format!("{}: has no bounded summary", entry.code));
577 }
578 if entry.category.is_some() && entry.severity != DiagnosticSeverity::Error {
583 problems.push(format!(
584 "{}: {} severity declares an error category",
585 entry.code,
586 entry.severity.as_str()
587 ));
588 }
589 if !retired
595 && entry.namespace() == "REQUEST"
596 && let Some(category) = entry.category
597 && category != ErrorCategory::Request
598 {
599 problems.push(format!(
600 "{}: REQUEST namespace declares category {category:?}, not Request",
601 entry.code
602 ));
603 }
604 if !seen.insert(entry.code) {
605 problems.push(format!("{}: registered twice", entry.code));
606 }
607 }
608 problems
609}
610
611pub fn check_scope_ownership(registries: &[(&str, &[&DiagnosticInfo])]) -> Vec<String> {
613 let mut owners: BTreeMap<(&str, &str), &str> = BTreeMap::new();
614 let mut problems = Vec::new();
615 for (crate_name, entries) in registries {
616 for entry in *entries {
617 if matches!(entry.status, CodeStatus::Retired { .. }) {
618 continue;
619 }
620 let mut segments = entry.code.split('.');
621 let (Some(namespace), Some(scope)) = (segments.next(), segments.next()) else {
622 continue;
623 };
624 match owners.entry((namespace, scope)) {
625 Entry::Vacant(slot) => {
626 slot.insert(crate_name);
627 }
628 Entry::Occupied(slot) if *slot.get() != *crate_name => problems.push(format!(
629 "{namespace}.{scope}: claimed by both {} and {crate_name}",
630 slot.get()
631 )),
632 Entry::Occupied(_) => {}
633 }
634 }
635 }
636 problems
637}
638
639#[must_use]
641pub fn code_is_well_formed(code: &str) -> bool {
642 let mut segments = 0usize;
643 for (index, segment) in code.split('.').enumerate() {
644 segments += 1;
645 if segment.is_empty() {
646 return false;
647 }
648 if index == 0 && !segment.starts_with(|character: char| character.is_ascii_uppercase()) {
649 return false;
650 }
651 if !segment
652 .bytes()
653 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
654 {
655 return false;
656 }
657 }
658 segments >= 3
659}
660
661#[must_use]
662pub fn render_diagnostic(diagnostic: &Diagnostic) -> String {
663 format!("{}: {}", diagnostic.code(), diagnostic.message())
664}
665
666#[must_use]
667pub fn render_diagnostics(diagnostics: &[Diagnostic]) -> Vec<String> {
668 diagnostics.iter().map(render_diagnostic).collect()
669}
670
671fn bounded_identifier(value: &str) -> String {
672 const LIMIT: usize = 160;
673 if value.len() <= LIMIT {
674 return value.to_owned();
675 }
676 let mut end = LIMIT;
677 while !value.is_char_boundary(end) {
678 end -= 1;
679 }
680 format!("{}…", &value[..end])
681}
682
683#[cfg(test)]
684mod tests {
685
686 #[test]
687 fn suggested_action_is_part_of_a_finding_identity() {
688 let base = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message");
689 let advised = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message")
690 .with_suggested_action("rebuild the series");
691 assert_ne!(base, advised, "advice changes what the record says to do");
692 assert_eq!(
693 advised,
694 Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message")
695 .with_suggested_action("rebuild the series")
696 );
697 }
698
699 #[test]
700 fn a_target_is_stored_complete_or_refused() {
701 let long = format!("/model/buses/{}", "a".repeat(4_000));
702 let kept = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "long target")
703 .with_target(long.clone())
704 .unwrap();
705 assert_eq!(kept.target(), Some(long.as_str()));
706
707 let oversize = "/".repeat(crate::validation::MAX_DIAGNOSTIC_TARGET_BYTES + 1);
711 let mut record = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "oversize");
712 assert!(record.set_target(oversize).is_err());
713 assert!(record.set_target("").is_err());
714 assert!(record.set_target("with\0nul").is_err());
715 assert_eq!(record.target(), None);
716 assert!(
717 Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "oversize")
718 .with_target("/".repeat(crate::validation::MAX_DIAGNOSTIC_TARGET_BYTES + 1))
719 .is_err()
720 );
721 }
722
723 #[test]
724 fn builder_paths_enforce_the_same_count_limits_as_the_decoder() {
725 let mut record = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "caps");
726 for index in 0..crate::validation::MAX_DIAGNOSTIC_RELATED {
727 record = record
728 .with_related(DiagnosticId::new(format!("d{index}")).unwrap())
729 .unwrap();
730 }
731 assert!(
732 record
733 .clone()
734 .with_related(DiagnosticId::new("one-too-many").unwrap())
735 .is_err()
736 );
737
738 for index in 0..crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS {
739 record
740 .insert_detail(format!("k{index}"), serde_json::Value::Null)
741 .unwrap();
742 }
743 assert!(
744 record
745 .insert_detail("one-too-many", serde_json::Value::Null)
746 .is_err()
747 );
748 assert!(
750 record
751 .insert_detail("k0", serde_json::Value::Bool(true))
752 .is_ok()
753 );
754 assert!(
755 record
756 .insert_detail("bad\0key", serde_json::Value::Null)
757 .is_err()
758 );
759 }
760
761 #[test]
762 fn stored_findings_meet_the_limits_the_constructors_enforce() {
763 let mut document = serde_json::json!({
764 "code": "PARTNER.TEST.FINDING",
765 "severity": "warning",
766 "message": "line one\nline two",
767 });
768 let record: Diagnostic = serde_json::from_value(document.clone()).unwrap();
769 assert_eq!(record.message(), "line one line two");
770
771 document["related"] = serde_json::Value::Array(
772 (0..=crate::validation::MAX_DIAGNOSTIC_RELATED)
773 .map(|index| serde_json::Value::String(format!("d{index}")))
774 .collect(),
775 );
776 assert!(serde_json::from_value::<Diagnostic>(document.clone()).is_err());
777
778 document["related"] = serde_json::Value::Array(Vec::new());
779 document["target"] = serde_json::Value::String(String::new());
780 assert!(serde_json::from_value::<Diagnostic>(document).is_err());
781 }
782
783 #[test]
784 fn stored_detail_keys_meet_the_key_predicate_the_constructors_enforce() {
785 let decode = |details: &str| {
786 serde_json::from_str::<Diagnostic>(&format!(
787 r#"{{"code":"PARTNER.TEST.FINDING","severity":"warning","message":"m","details":{details}}}"#
788 ))
789 };
790 assert!(decode(r#"{"":1}"#).is_err());
793 assert!(decode("{\"bad\\u0000key\":1}").is_err());
794
795 let full: Vec<String> = (0..crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS)
797 .map(|index| format!(r#""k{index}":{index}"#))
798 .collect();
799 let at_limit = decode(&format!("{{{}}}", full.join(","))).unwrap();
800 assert_eq!(
801 at_limit.details().len(),
802 crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS
803 );
804 let over: Vec<String> = (0..=crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS)
805 .map(|index| format!(r#""k{index}":{index}"#))
806 .collect();
807 assert!(decode(&format!("{{{}}}", over.join(","))).is_err());
808
809 let rebuilt = Diagnostic::of(&crate::codes::REQUEST_RECORD_TOO_LARGE, "m")
811 .with_details(at_limit.details().clone());
812 assert!(rebuilt.is_ok());
813 }
814 use super::*;
815
816 #[test]
817 fn exact_severity_and_category_tokens_are_closed() {
818 assert_eq!(
819 ErrorCategory::ALL.map(ErrorCategory::as_str),
820 ["io", "request", "parse", "data", "output"]
821 );
822 let severities = [
823 DiagnosticSeverity::Error,
824 DiagnosticSeverity::Warning,
825 DiagnosticSeverity::Remark,
826 DiagnosticSeverity::Note,
827 ];
828 assert_eq!(severities.len(), 4);
829 }
830
831 #[test]
832 fn transform_namespace_is_exact() {
833 assert_eq!(DiagnosticStage::Transform.namespace(), "TRANSFORM");
834 assert_eq!(DiagnosticStage::from_namespace("EXTERNAL"), None);
835 assert_eq!(DiagnosticStage::ALL.len(), 10);
836 }
837
838 #[test]
839 fn code_grammar_rejects_malformed_and_oversized_input() {
840 for code in [
841 "TRANSFORM.DIST.UNKNOWN_BUS",
842 "READ.DSS.INCLUDE_REFUSED",
843 "EMIT.BMOPF.TRANSFORMER.TAP_COLLAPSED",
844 ] {
845 assert!(code_is_well_formed(code), "{code}");
846 assert!(DiagnosticCode::new(code).is_ok());
847 }
848 for code in ["", "READ.DSS", "read.dss.bad", "READ..BAD", "1READ.DSS.BAD"] {
849 assert!(DiagnosticCode::new(code).is_err(), "{code}");
850 }
851 let external_namespace = DiagnosticCode::new("EXTERNAL.DIST.UNKNOWN_BUS").unwrap();
852 assert_eq!(external_namespace.stage(), None);
853 let long = format!("READ.DSS.{}", "A".repeat(MAX_DIAGNOSTIC_CODE_BYTES));
854 assert!(DiagnosticCode::new(long).is_err());
855 }
856
857 #[test]
858 fn rendering_bounds_external_text_and_keeps_one_line() {
859 let diagnostic = Diagnostic::of(
860 &crate::codes::REQUEST_DIAGNOSTIC_INVALID_CODE,
861 format!("first\n{}", "x".repeat(20_000)),
862 );
863 let line = render_diagnostic(&diagnostic);
864 assert!(!line.contains(['\n', '\r']));
865 assert!(line.len() < 17_000);
866 }
867}
868
869mod wire {
875 use serde::de::DeserializeSeed;
876 use serde::{Deserialize, Deserializer, Serialize};
877 use serde_json::{Map, Value};
878
879 use super::{Diagnostic, DiagnosticCode, DiagnosticIdentity, DiagnosticSeverity};
880 use crate::bounded::{BoundedStr, TruncatedStr, bounded_json_map, bounded_vec};
881 use crate::validation::{
882 MAX_DIAGNOSTIC_CODE_BYTES, MAX_DIAGNOSTIC_DETAIL_KEYS, MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
883 MAX_DIAGNOSTIC_RELATED, MAX_DIAGNOSTIC_SPANS, MAX_DIAGNOSTIC_TARGET_BYTES,
884 MAX_IDENTIFIER_BYTES,
885 };
886 use crate::{DiagnosticId, SourceSpan};
887
888 #[derive(Serialize, Deserialize)]
892 pub(super) struct DiagnosticWire {
893 #[serde(deserialize_with = "de_code")]
894 code: String,
895 severity: DiagnosticSeverity,
896 #[serde(deserialize_with = "de_message")]
897 message: String,
898 #[serde(default, skip_serializing_if = "Option::is_none")]
899 id: Option<DiagnosticId>,
900 #[serde(
901 default,
902 skip_serializing_if = "Option::is_none",
903 deserialize_with = "de_target"
904 )]
905 target: Option<String>,
906 #[serde(
907 default,
908 skip_serializing_if = "Vec::is_empty",
909 deserialize_with = "de_spans"
910 )]
911 spans: Vec<SourceSpan>,
912 #[serde(
913 default,
914 skip_serializing_if = "Vec::is_empty",
915 deserialize_with = "de_related"
916 )]
917 related: Vec<DiagnosticId>,
918 #[serde(
919 default,
920 skip_serializing_if = "Map::is_empty",
921 deserialize_with = "de_details"
922 )]
923 details: Map<String, Value>,
924 #[serde(
925 default,
926 skip_serializing_if = "Option::is_none",
927 deserialize_with = "de_action"
928 )]
929 suggested_action: Option<String>,
930 }
931
932 fn de_code<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
933 BoundedStr {
934 what: "diagnostic code",
935 max_bytes: MAX_DIAGNOSTIC_CODE_BYTES,
936 }
937 .deserialize(deserializer)
938 }
939
940 fn de_message<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
941 deserializer.deserialize_str(TruncatedStr {
942 max_bytes: MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
943 })
944 }
945
946 fn de_target<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
947 BoundedStr {
948 what: "diagnostic target",
949 max_bytes: MAX_DIAGNOSTIC_TARGET_BYTES,
950 }
951 .deserialize(deserializer)
952 .map(Some)
953 }
954
955 fn de_action<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
956 deserializer
957 .deserialize_str(TruncatedStr {
958 max_bytes: MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
959 })
960 .map(Some)
961 }
962
963 fn de_spans<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<SourceSpan>, D::Error> {
964 bounded_vec(deserializer, "source spans", MAX_DIAGNOSTIC_SPANS)
965 }
966
967 fn de_related<'de, D: Deserializer<'de>>(
968 deserializer: D,
969 ) -> Result<Vec<DiagnosticId>, D::Error> {
970 bounded_vec(deserializer, "related records", MAX_DIAGNOSTIC_RELATED)
971 }
972
973 fn de_details<'de, D: Deserializer<'de>>(
974 deserializer: D,
975 ) -> Result<Map<String, Value>, D::Error> {
976 bounded_json_map(
977 deserializer,
978 "detail keys",
979 MAX_DIAGNOSTIC_DETAIL_KEYS,
980 MAX_IDENTIFIER_BYTES,
981 crate::validation::valid_nonempty_text,
982 )
983 }
984
985 impl From<&Diagnostic> for DiagnosticWire {
986 fn from(diagnostic: &Diagnostic) -> Self {
987 Self {
988 code: diagnostic.code().to_owned(),
989 severity: diagnostic.severity,
990 message: diagnostic.message.clone(),
991 id: diagnostic.id.clone(),
992 target: diagnostic.target.clone(),
993 spans: diagnostic.spans.clone(),
994 related: diagnostic.related.clone(),
995 details: diagnostic.details.clone(),
996 suggested_action: diagnostic.suggested_action.clone(),
997 }
998 }
999 }
1000
1001 impl TryFrom<DiagnosticWire> for Diagnostic {
1002 type Error = crate::Error;
1003
1004 fn try_from(wire: DiagnosticWire) -> Result<Self, Self::Error> {
1008 use crate::validation::{
1009 MAX_DIAGNOSTIC_RELATED, MAX_DIAGNOSTIC_SPANS, sanitize_message,
1010 valid_diagnostic_target,
1011 };
1012
1013 let refuse = |what: &str, limit: usize| {
1014 crate::Error::new(
1015 &crate::codes::REQUEST_RECORD_TOO_LARGE,
1016 format!("a stored diagnostic carries more than {limit} {what}"),
1017 )
1018 };
1019 if wire.spans.len() > MAX_DIAGNOSTIC_SPANS {
1020 return Err(refuse("source spans", MAX_DIAGNOSTIC_SPANS));
1021 }
1022 if wire.related.len() > MAX_DIAGNOSTIC_RELATED {
1023 return Err(refuse("related records", MAX_DIAGNOSTIC_RELATED));
1024 }
1025 super::check_details(&wire.details)?;
1028 let target = match wire.target {
1029 Some(target) if !valid_diagnostic_target(&target) => {
1030 return Err(crate::Error::new(
1031 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
1032 "a stored diagnostic target is empty or oversized",
1033 ));
1034 }
1035 other => other,
1036 };
1037 Ok(Diagnostic {
1038 identity: DiagnosticIdentity::External(DiagnosticCode::new(wire.code)?),
1039 id: wire.id,
1040 severity: wire.severity,
1041 message: sanitize_message(wire.message),
1042 target,
1043 spans: wire.spans,
1044 related: wire.related,
1045 details: wire.details,
1046 suggested_action: wire.suggested_action.map(sanitize_message),
1047 })
1048 }
1049 }
1050}
1051
1052impl serde::Serialize for Diagnostic {
1053 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1054 wire::DiagnosticWire::from(self).serialize(serializer)
1055 }
1056}
1057
1058impl<'de> serde::Deserialize<'de> for Diagnostic {
1059 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1060 let wire = wire::DiagnosticWire::deserialize(deserializer)?;
1061 Self::try_from(wire).map_err(serde::de::Error::custom)
1062 }
1063}