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]
387 pub fn with_severity(mut self, severity: DiagnosticSeverity) -> Self {
388 self.severity = severity;
389 self
390 }
391
392 pub fn with_target(mut self, target: impl Into<String>) -> Result<Self, Error> {
399 self.set_target(target)?;
400 Ok(self)
401 }
402
403 #[must_use]
405 pub fn target_is_pointer(&self) -> bool {
406 self.target
407 .as_deref()
408 .is_some_and(crate::validation::valid_rfc6901_pointer)
409 }
410
411 pub fn with_span(mut self, span: SourceSpan) -> Result<Self, Error> {
412 if self.spans.len() >= crate::validation::MAX_DIAGNOSTIC_SPANS {
413 return Err(record_too_large(
414 "source spans",
415 crate::validation::MAX_DIAGNOSTIC_SPANS,
416 ));
417 }
418 self.spans.push(span);
419 Ok(self)
420 }
421
422 pub fn with_related(mut self, related: DiagnosticId) -> Result<Self, Error> {
423 if self.related.len() >= crate::validation::MAX_DIAGNOSTIC_RELATED {
424 return Err(record_too_large(
425 "related records",
426 crate::validation::MAX_DIAGNOSTIC_RELATED,
427 ));
428 }
429 self.related.push(related);
430 Ok(self)
431 }
432
433 pub fn insert_detail(&mut self, key: impl Into<String>, value: Value) -> Result<(), Error> {
436 let key = key.into();
437 if !crate::validation::valid_nonempty_text(&key) {
438 return Err(Error::new(
439 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
440 "a diagnostic detail key must be nonempty and bounded",
441 ));
442 }
443 if !self.details.contains_key(&key)
444 && self.details.len() >= crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS
445 {
446 return Err(record_too_large(
447 "detail keys",
448 crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS,
449 ));
450 }
451 self.details.insert(key, value);
452 Ok(())
453 }
454
455 pub fn clear_target(&mut self) {
459 self.target = None;
460 }
461
462 pub fn set_target(&mut self, target: impl Into<String>) -> Result<(), Error> {
466 let target = target.into();
467 if !crate::validation::valid_diagnostic_target(&target) {
468 return Err(Error::new(
469 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
470 "a diagnostic target must be a nonempty bounded locator",
471 ));
472 }
473 self.target = Some(target);
474 Ok(())
475 }
476
477 pub fn with_details(mut self, details: Map<String, Value>) -> Result<Self, Error> {
478 self.set_details(details)?;
479 Ok(self)
480 }
481
482 pub fn set_details(&mut self, details: Map<String, Value>) -> Result<(), Error> {
485 check_details(&details)?;
486 self.details = details;
487 Ok(())
488 }
489
490 #[must_use]
492 pub fn suggested_action(&self) -> Option<&str> {
493 self.suggested_action.as_deref()
494 }
495
496 #[must_use]
497 pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
498 self.suggested_action = Some(sanitize_message(action));
499 self
500 }
501}
502
503impl PartialEq for Diagnostic {
504 fn eq(&self, other: &Self) -> bool {
505 self.code() == other.code()
506 && self.id == other.id
507 && self.severity == other.severity
508 && self.message == other.message
509 && self.target == other.target
510 && self.spans == other.spans
511 && self.related == other.related
512 && self.details == other.details
513 && self.suggested_action == other.suggested_action
514 }
515}
516
517pub fn check_registry<'a, I>(entries: I) -> Vec<String>
519where
520 I: IntoIterator<Item = &'a DiagnosticInfo>,
521{
522 let mut problems = Vec::new();
523 let mut seen = BTreeSet::new();
524 for entry in entries {
525 let retired = matches!(entry.status, CodeStatus::Retired { .. });
526 if !retired
527 && (!code_is_well_formed(entry.code) || entry.code.len() > MAX_DIAGNOSTIC_CODE_BYTES)
528 {
529 problems.push(format!("{}: does not match the code grammar", entry.code));
530 } else if !retired && entry.stage().is_none() {
531 problems.push(format!(
532 "{}: namespace {} is not one PowerIO emits",
533 entry.code,
534 entry.namespace()
535 ));
536 }
537 if !valid_nonempty_text(entry.summary) {
538 problems.push(format!("{}: has no bounded summary", entry.code));
539 }
540 if entry.category.is_some() && entry.severity != DiagnosticSeverity::Error {
545 problems.push(format!(
546 "{}: {} severity declares an error category",
547 entry.code,
548 entry.severity.as_str()
549 ));
550 }
551 if !retired
557 && entry.namespace() == "REQUEST"
558 && let Some(category) = entry.category
559 && category != ErrorCategory::Request
560 {
561 problems.push(format!(
562 "{}: REQUEST namespace declares category {category:?}, not Request",
563 entry.code
564 ));
565 }
566 if !seen.insert(entry.code) {
567 problems.push(format!("{}: registered twice", entry.code));
568 }
569 }
570 problems
571}
572
573pub fn check_scope_ownership(registries: &[(&str, &[&DiagnosticInfo])]) -> Vec<String> {
575 let mut owners: BTreeMap<(&str, &str), &str> = BTreeMap::new();
576 let mut problems = Vec::new();
577 for (crate_name, entries) in registries {
578 for entry in *entries {
579 if matches!(entry.status, CodeStatus::Retired { .. }) {
580 continue;
581 }
582 let mut segments = entry.code.split('.');
583 let (Some(namespace), Some(scope)) = (segments.next(), segments.next()) else {
584 continue;
585 };
586 match owners.entry((namespace, scope)) {
587 Entry::Vacant(slot) => {
588 slot.insert(crate_name);
589 }
590 Entry::Occupied(slot) if *slot.get() != *crate_name => problems.push(format!(
591 "{namespace}.{scope}: claimed by both {} and {crate_name}",
592 slot.get()
593 )),
594 Entry::Occupied(_) => {}
595 }
596 }
597 }
598 problems
599}
600
601#[must_use]
603pub fn code_is_well_formed(code: &str) -> bool {
604 let mut segments = 0usize;
605 for (index, segment) in code.split('.').enumerate() {
606 segments += 1;
607 if segment.is_empty() {
608 return false;
609 }
610 if index == 0 && !segment.starts_with(|character: char| character.is_ascii_uppercase()) {
611 return false;
612 }
613 if !segment
614 .bytes()
615 .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
616 {
617 return false;
618 }
619 }
620 segments >= 3
621}
622
623#[must_use]
624pub fn render_diagnostic(diagnostic: &Diagnostic) -> String {
625 format!("{}: {}", diagnostic.code(), diagnostic.message())
626}
627
628#[must_use]
629pub fn render_diagnostics(diagnostics: &[Diagnostic]) -> Vec<String> {
630 diagnostics.iter().map(render_diagnostic).collect()
631}
632
633fn bounded_identifier(value: &str) -> String {
634 const LIMIT: usize = 160;
635 if value.len() <= LIMIT {
636 return value.to_owned();
637 }
638 let mut end = LIMIT;
639 while !value.is_char_boundary(end) {
640 end -= 1;
641 }
642 format!("{}…", &value[..end])
643}
644
645#[cfg(test)]
646mod tests {
647
648 #[test]
649 fn suggested_action_is_part_of_a_finding_identity() {
650 let base = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message");
651 let advised = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message")
652 .with_suggested_action("rebuild the series");
653 assert_ne!(base, advised, "advice changes what the record says to do");
654 assert_eq!(
655 advised,
656 Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "same message")
657 .with_suggested_action("rebuild the series")
658 );
659 }
660
661 #[test]
662 fn a_target_is_stored_complete_or_refused() {
663 let long = format!("/model/buses/{}", "a".repeat(4_000));
664 let kept = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "long target")
665 .with_target(long.clone())
666 .unwrap();
667 assert_eq!(kept.target(), Some(long.as_str()));
668
669 let oversize = "/".repeat(crate::validation::MAX_DIAGNOSTIC_TARGET_BYTES + 1);
673 let mut record = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "oversize");
674 assert!(record.set_target(oversize).is_err());
675 assert!(record.set_target("").is_err());
676 assert!(record.set_target("with\0nul").is_err());
677 assert_eq!(record.target(), None);
678 assert!(
679 Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "oversize")
680 .with_target("/".repeat(crate::validation::MAX_DIAGNOSTIC_TARGET_BYTES + 1))
681 .is_err()
682 );
683 }
684
685 #[test]
686 fn builder_paths_enforce_the_same_count_limits_as_the_decoder() {
687 let mut record = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "caps");
688 for index in 0..crate::validation::MAX_DIAGNOSTIC_RELATED {
689 record = record
690 .with_related(DiagnosticId::new(format!("d{index}")).unwrap())
691 .unwrap();
692 }
693 assert!(
694 record
695 .clone()
696 .with_related(DiagnosticId::new("one-too-many").unwrap())
697 .is_err()
698 );
699
700 for index in 0..crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS {
701 record
702 .insert_detail(format!("k{index}"), serde_json::Value::Null)
703 .unwrap();
704 }
705 assert!(
706 record
707 .insert_detail("one-too-many", serde_json::Value::Null)
708 .is_err()
709 );
710 assert!(
712 record
713 .insert_detail("k0", serde_json::Value::Bool(true))
714 .is_ok()
715 );
716 assert!(
717 record
718 .insert_detail("bad\0key", serde_json::Value::Null)
719 .is_err()
720 );
721 }
722
723 #[test]
724 fn stored_findings_meet_the_limits_the_constructors_enforce() {
725 let mut document = serde_json::json!({
726 "code": "PARTNER.TEST.FINDING",
727 "severity": "warning",
728 "message": "line one\nline two",
729 });
730 let record: Diagnostic = serde_json::from_value(document.clone()).unwrap();
731 assert_eq!(record.message(), "line one line two");
732
733 document["related"] = serde_json::Value::Array(
734 (0..=crate::validation::MAX_DIAGNOSTIC_RELATED)
735 .map(|index| serde_json::Value::String(format!("d{index}")))
736 .collect(),
737 );
738 assert!(serde_json::from_value::<Diagnostic>(document.clone()).is_err());
739
740 document["related"] = serde_json::Value::Array(Vec::new());
741 document["target"] = serde_json::Value::String(String::new());
742 assert!(serde_json::from_value::<Diagnostic>(document).is_err());
743 }
744
745 #[test]
746 fn stored_detail_keys_meet_the_key_predicate_the_constructors_enforce() {
747 let decode = |details: &str| {
748 serde_json::from_str::<Diagnostic>(&format!(
749 r#"{{"code":"PARTNER.TEST.FINDING","severity":"warning","message":"m","details":{details}}}"#
750 ))
751 };
752 assert!(decode(r#"{"":1}"#).is_err());
755 assert!(decode("{\"bad\\u0000key\":1}").is_err());
756
757 let full: Vec<String> = (0..crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS)
759 .map(|index| format!(r#""k{index}":{index}"#))
760 .collect();
761 let at_limit = decode(&format!("{{{}}}", full.join(","))).unwrap();
762 assert_eq!(
763 at_limit.details().len(),
764 crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS
765 );
766 let over: Vec<String> = (0..=crate::validation::MAX_DIAGNOSTIC_DETAIL_KEYS)
767 .map(|index| format!(r#""k{index}":{index}"#))
768 .collect();
769 assert!(decode(&format!("{{{}}}", over.join(","))).is_err());
770
771 let rebuilt = Diagnostic::of(&crate::codes::REQUEST_RECORD_TOO_LARGE, "m")
773 .with_details(at_limit.details().clone());
774 assert!(rebuilt.is_ok());
775 }
776 use super::*;
777
778 #[test]
779 fn exact_severity_and_category_tokens_are_closed() {
780 assert_eq!(
781 ErrorCategory::ALL.map(ErrorCategory::as_str),
782 ["io", "request", "parse", "data", "output"]
783 );
784 let severities = [
785 DiagnosticSeverity::Error,
786 DiagnosticSeverity::Warning,
787 DiagnosticSeverity::Remark,
788 DiagnosticSeverity::Note,
789 ];
790 assert_eq!(severities.len(), 4);
791 }
792
793 #[test]
794 fn transform_namespace_is_exact() {
795 assert_eq!(DiagnosticStage::Transform.namespace(), "TRANSFORM");
796 assert_eq!(DiagnosticStage::from_namespace("EXTERNAL"), None);
797 assert_eq!(DiagnosticStage::ALL.len(), 10);
798 }
799
800 #[test]
801 fn code_grammar_rejects_malformed_and_oversized_input() {
802 for code in [
803 "TRANSFORM.DIST.UNKNOWN_BUS",
804 "READ.DSS.INCLUDE_REFUSED",
805 "EMIT.BMOPF.TRANSFORMER.TAP_COLLAPSED",
806 ] {
807 assert!(code_is_well_formed(code), "{code}");
808 assert!(DiagnosticCode::new(code).is_ok());
809 }
810 for code in ["", "READ.DSS", "read.dss.bad", "READ..BAD", "1READ.DSS.BAD"] {
811 assert!(DiagnosticCode::new(code).is_err(), "{code}");
812 }
813 let external_namespace = DiagnosticCode::new("EXTERNAL.DIST.UNKNOWN_BUS").unwrap();
814 assert_eq!(external_namespace.stage(), None);
815 let long = format!("READ.DSS.{}", "A".repeat(MAX_DIAGNOSTIC_CODE_BYTES));
816 assert!(DiagnosticCode::new(long).is_err());
817 }
818
819 #[test]
820 fn rendering_bounds_external_text_and_keeps_one_line() {
821 let diagnostic = Diagnostic::of(
822 &crate::codes::REQUEST_DIAGNOSTIC_INVALID_CODE,
823 format!("first\n{}", "x".repeat(20_000)),
824 );
825 let line = render_diagnostic(&diagnostic);
826 assert!(!line.contains(['\n', '\r']));
827 assert!(line.len() < 17_000);
828 }
829}
830
831mod wire {
837 use serde::de::DeserializeSeed;
838 use serde::{Deserialize, Deserializer, Serialize};
839 use serde_json::{Map, Value};
840
841 use super::{Diagnostic, DiagnosticCode, DiagnosticIdentity, DiagnosticSeverity};
842 use crate::bounded::{BoundedStr, TruncatedStr, bounded_json_map, bounded_vec};
843 use crate::validation::{
844 MAX_DIAGNOSTIC_CODE_BYTES, MAX_DIAGNOSTIC_DETAIL_KEYS, MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
845 MAX_DIAGNOSTIC_RELATED, MAX_DIAGNOSTIC_SPANS, MAX_DIAGNOSTIC_TARGET_BYTES,
846 MAX_IDENTIFIER_BYTES,
847 };
848 use crate::{DiagnosticId, SourceSpan};
849
850 #[derive(Serialize, Deserialize)]
854 pub(super) struct DiagnosticWire {
855 #[serde(deserialize_with = "de_code")]
856 code: String,
857 severity: DiagnosticSeverity,
858 #[serde(deserialize_with = "de_message")]
859 message: String,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
861 id: Option<DiagnosticId>,
862 #[serde(
863 default,
864 skip_serializing_if = "Option::is_none",
865 deserialize_with = "de_target"
866 )]
867 target: Option<String>,
868 #[serde(
869 default,
870 skip_serializing_if = "Vec::is_empty",
871 deserialize_with = "de_spans"
872 )]
873 spans: Vec<SourceSpan>,
874 #[serde(
875 default,
876 skip_serializing_if = "Vec::is_empty",
877 deserialize_with = "de_related"
878 )]
879 related: Vec<DiagnosticId>,
880 #[serde(
881 default,
882 skip_serializing_if = "Map::is_empty",
883 deserialize_with = "de_details"
884 )]
885 details: Map<String, Value>,
886 #[serde(
887 default,
888 skip_serializing_if = "Option::is_none",
889 deserialize_with = "de_action"
890 )]
891 suggested_action: Option<String>,
892 }
893
894 fn de_code<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
895 BoundedStr {
896 what: "diagnostic code",
897 max_bytes: MAX_DIAGNOSTIC_CODE_BYTES,
898 }
899 .deserialize(deserializer)
900 }
901
902 fn de_message<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
903 deserializer.deserialize_str(TruncatedStr {
904 max_bytes: MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
905 })
906 }
907
908 fn de_target<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
909 BoundedStr {
910 what: "diagnostic target",
911 max_bytes: MAX_DIAGNOSTIC_TARGET_BYTES,
912 }
913 .deserialize(deserializer)
914 .map(Some)
915 }
916
917 fn de_action<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<String>, D::Error> {
918 deserializer
919 .deserialize_str(TruncatedStr {
920 max_bytes: MAX_DIAGNOSTIC_MESSAGE_DECODE_BYTES,
921 })
922 .map(Some)
923 }
924
925 fn de_spans<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<SourceSpan>, D::Error> {
926 bounded_vec(deserializer, "source spans", MAX_DIAGNOSTIC_SPANS)
927 }
928
929 fn de_related<'de, D: Deserializer<'de>>(
930 deserializer: D,
931 ) -> Result<Vec<DiagnosticId>, D::Error> {
932 bounded_vec(deserializer, "related records", MAX_DIAGNOSTIC_RELATED)
933 }
934
935 fn de_details<'de, D: Deserializer<'de>>(
936 deserializer: D,
937 ) -> Result<Map<String, Value>, D::Error> {
938 bounded_json_map(
939 deserializer,
940 "detail keys",
941 MAX_DIAGNOSTIC_DETAIL_KEYS,
942 MAX_IDENTIFIER_BYTES,
943 crate::validation::valid_nonempty_text,
944 )
945 }
946
947 impl From<&Diagnostic> for DiagnosticWire {
948 fn from(diagnostic: &Diagnostic) -> Self {
949 Self {
950 code: diagnostic.code().to_owned(),
951 severity: diagnostic.severity,
952 message: diagnostic.message.clone(),
953 id: diagnostic.id.clone(),
954 target: diagnostic.target.clone(),
955 spans: diagnostic.spans.clone(),
956 related: diagnostic.related.clone(),
957 details: diagnostic.details.clone(),
958 suggested_action: diagnostic.suggested_action.clone(),
959 }
960 }
961 }
962
963 impl TryFrom<DiagnosticWire> for Diagnostic {
964 type Error = crate::Error;
965
966 fn try_from(wire: DiagnosticWire) -> Result<Self, Self::Error> {
970 use crate::validation::{
971 MAX_DIAGNOSTIC_RELATED, MAX_DIAGNOSTIC_SPANS, sanitize_message,
972 valid_diagnostic_target,
973 };
974
975 let refuse = |what: &str, limit: usize| {
976 crate::Error::new(
977 &crate::codes::REQUEST_RECORD_TOO_LARGE,
978 format!("a stored diagnostic carries more than {limit} {what}"),
979 )
980 };
981 if wire.spans.len() > MAX_DIAGNOSTIC_SPANS {
982 return Err(refuse("source spans", MAX_DIAGNOSTIC_SPANS));
983 }
984 if wire.related.len() > MAX_DIAGNOSTIC_RELATED {
985 return Err(refuse("related records", MAX_DIAGNOSTIC_RELATED));
986 }
987 super::check_details(&wire.details)?;
990 let target = match wire.target {
991 Some(target) if !valid_diagnostic_target(&target) => {
992 return Err(crate::Error::new(
993 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
994 "a stored diagnostic target is empty or oversized",
995 ));
996 }
997 other => other,
998 };
999 Ok(Diagnostic {
1000 identity: DiagnosticIdentity::External(DiagnosticCode::new(wire.code)?),
1001 id: wire.id,
1002 severity: wire.severity,
1003 message: sanitize_message(wire.message),
1004 target,
1005 spans: wire.spans,
1006 related: wire.related,
1007 details: wire.details,
1008 suggested_action: wire.suggested_action.map(sanitize_message),
1009 })
1010 }
1011 }
1012}
1013
1014impl serde::Serialize for Diagnostic {
1015 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1016 wire::DiagnosticWire::from(self).serialize(serializer)
1017 }
1018}
1019
1020impl<'de> serde::Deserialize<'de> for Diagnostic {
1021 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1022 let wire = wire::DiagnosticWire::deserialize(deserializer)?;
1023 Self::try_from(wire).map_err(serde::de::Error::custom)
1024 }
1025}