edi_energy/report.rs
1use std::fmt;
2
3use edifact_rs::{ValidationIssue, ValidationReport};
4
5/// Classify a rule ID into a validation layer origin string.
6///
7/// Used to populate [`ValidationIssueSummary::rule_origin`].
8///
9/// Returns `None` when `rule_id` is `None` or does not match a known prefix.
10fn classify_rule_origin(rule_id: &str) -> Option<&'static str> {
11 if rule_id.starts_with("MIG-") || rule_id.starts_with("UNKNOWN-MSG-TYPE") {
12 Some("mig")
13 } else if rule_id.starts_with("AHB-") || rule_id.starts_with("AHB-SKIP-") {
14 Some("ahb")
15 } else if rule_id.starts_with("SEM-") {
16 Some("semantic")
17 } else if rule_id.starts_with("PARSE-") {
18 Some("parse")
19 } else if rule_id.starts_with("DIR-") {
20 Some("directory")
21 } else if rule_id.starts_with("CUSTOM-") {
22 Some("custom")
23 } else {
24 None
25 }
26}
27
28/// The mako-facing projection of a single [`edifact_rs::ValidationIssue`].
29///
30/// `ValidationIssue` is already owned and serializable; this type exists to shape
31/// the *mako* serialization contract — camelCase field names, a curated field set,
32/// severity as a stable string, and two derived fields the raw issue does not carry
33/// (`rule_origin` and the resolved `pruefidentifikator`).
34///
35/// Unconditionally available (no feature gate required). The `serde` feature
36/// adds `#[derive(Serialize)]` so instances can be JSON-encoded directly.
37#[derive(Debug, Clone, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize))]
39#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
40pub struct ValidationIssueSummary {
41 /// Severity string: `"critical"`, `"error"`, `"warning"`, or `"info"`.
42 pub severity: &'static str,
43 /// Human-readable description of the issue.
44 pub message: String,
45 /// Stable rule identifier (e.g. `"MIG-DTM-001"` or `"AHB-13001-STS-I0"`),
46 /// if available.
47 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
48 pub rule_id: Option<String>,
49 /// Stable error code assigned by the validator, if any.
50 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
51 pub error_code: Option<String>,
52 /// Tag of the EDIFACT segment where the issue occurred, e.g. `"DTM"`.
53 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
54 pub segment_tag: Option<String>,
55 /// 0-based index of the occurrence among all segments with `segment_tag`.
56 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
57 pub segment_occurrence: Option<u16>,
58 /// Segment group name in which the issue occurred (e.g. `"SG4"`), if available.
59 ///
60 /// Populated from [`edifact_rs::ValidationIssue::segment_group`].
61 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
62 pub segment_group: Option<String>,
63 /// 0-based data-element index within the segment.
64 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
65 pub element_index: Option<u8>,
66 /// 0-based component index within a composite data element.
67 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
68 pub component_index: Option<u8>,
69 /// UNH message reference (DE 0062) the issue belongs to.
70 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
71 pub message_ref: Option<String>,
72 /// Suggested remediation text, if available.
73 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
74 pub suggestion: Option<String>,
75 /// Byte offset of the first byte of the affected region in the source input.
76 ///
77 /// The start of [`edifact_rs::ValidationIssue::span`].
78 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
79 pub offset: Option<usize>,
80 /// Exclusive byte offset of the end of the affected region in the source input.
81 ///
82 /// Together with [`offset`](Self::offset) this forms a half-open byte range
83 /// `[offset, byte_end)` that maps directly to an LSP `Range` or a `miette`
84 /// source span. `None` when the issue has no span.
85 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
86 pub byte_end: Option<usize>,
87 /// The BDEW Prüfidentifikator (process variant code) associated with this
88 /// issue, when the report was produced by a PID-specific validation layer.
89 ///
90 /// This field enables downstream audit logs and monitoring systems to
91 /// identify which BDEW process variant triggered a violation without
92 /// re-reading the raw EDIFACT message — satisfying the regulatory
93 /// traceability requirement for German energy market participants.
94 ///
95 /// `None` when the validation layer does not use PID-gated rule packs
96 /// (e.g. structural Layer 1–2 checks that apply to all process variants).
97 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
98 pub pruefidentifikator: Option<u32>,
99 /// Validation layer that generated this issue.
100 ///
101 /// Classifies the issue by its origin in the layered validation stack:
102 ///
103 /// | Value | Layer | Meaning |
104 /// |-------|-------|---------|
105 /// | `"parse"` | L1 | EDIFACT parse / tokenizer error |
106 /// | `"directory"` | L2 | Directory check (segment definitions, code lists) |
107 /// | `"mig"` | L3 | MIG structural rule (segment ordering, cardinality) |
108 /// | `"ahb"` | L4–L5 | AHB Bedingungsoperator rule |
109 /// | `"custom"` | L6 | Caller-supplied `CustomRulePack` rule |
110 ///
111 /// Used by monitoring dashboards and regulatory audit systems to distinguish
112 /// sender-side malformed EDI (L1–L2) from conformance violations (L3–L5)
113 /// from local business-rule failures (L6).
114 ///
115 /// Derived from the `rule_id` prefix heuristic:
116 /// - `"MIG-"` prefix → `"mig"`, `"AHB-"` → `"ahb"`, `"PARSE-"` → `"parse"`,
117 /// `"DIR-"` → `"directory"`, `"CUSTOM-"` → `"custom"`.
118 /// - `None` when the `rule_id` is absent or does not match a known prefix.
119 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
120 pub rule_origin: Option<&'static str>,
121}
122
123impl ValidationIssueSummary {
124 /// Convert a `ValidationIssue` to a `ValidationIssueSummary`, tagging it
125 /// with the Prüfidentifikator that was active during validation.
126 ///
127 /// The PID is resolved in priority order:
128 /// 1. `issue.context_get("pid")` — set per-issue by AHB rule closures via
129 /// `with_context_entry("pid", …)`.
130 /// 2. `pruefidentifikator` — report-level fallback for callers that set the
131 /// PID on the report rather than on individual issues.
132 ///
133 /// Use this from `EdiEnergyReport::serialize` so every serialized issue
134 /// carries the PID context needed for regulatory audit logs.
135 pub fn from_issue_with_pid(issue: &ValidationIssue, pruefidentifikator: Option<u32>) -> Self {
136 // Per-issue context: prefer the PID embedded in the issue itself.
137 let resolved_pid = issue
138 .context_get("pid")
139 .and_then(|s| s.parse::<u32>().ok())
140 .or(pruefidentifikator);
141
142 // Derive rule_origin from rule_id prefix.
143 let rule_origin = issue.rule_id.as_deref().and_then(classify_rule_origin);
144
145 Self {
146 // Use the as_str() helper instead of a
147 // hand-written match — future severity variants are handled automatically.
148 severity: issue.severity.as_str(),
149 message: issue.message.clone(),
150 rule_id: issue.rule_id.clone(),
151 // edifact-rs 0.13: error_code is now Option<Cow<'static, str>>, so it
152 // survives a serde round-trip; we own it here for a self-contained summary.
153 error_code: issue.error_code.as_deref().map(str::to_owned),
154 segment_tag: issue.segment_tag.clone(),
155 segment_occurrence: issue.segment_occurrence,
156 segment_group: issue.segment_group.as_deref().map(str::to_owned),
157 element_index: issue.element_index,
158 component_index: issue.component_index,
159 message_ref: issue.message_ref.clone(),
160 suggestion: issue.suggestion.clone(),
161 // edifact-rs 0.13: the redundant `offset` field was collapsed into `span`.
162 offset: issue.span.map(|s| s.start),
163 byte_end: issue.span.map(|s| s.end),
164 pruefidentifikator: resolved_pid,
165 rule_origin,
166 }
167 }
168}
169
170/// The result of validating an EDI@Energy message.
171///
172/// Wraps [`edifact_rs::ValidationReport`] with a stable, ergonomic API.
173/// Issues are divided into three severity buckets: errors, warnings, and infos.
174///
175/// A report is considered *valid* when it contains no error-level issues (warnings
176/// and infos are allowed).
177#[derive(Debug, Clone)]
178pub struct EdiEnergyReport {
179 inner: ValidationReport,
180 /// The Prüfidentifikator the message was validated against, if known.
181 ///
182 /// Set when validation is performed against a PID-specific AHB rule pack.
183 /// Propagated into each [`ValidationIssueSummary`] during serialization.
184 pruefidentifikator: Option<u32>,
185 /// The wire-format release code from the profile (e.g. `"2.4c"`, `"5.5.3a"`).
186 ///
187 /// Populated when validation is performed through the registry so audit logs
188 /// can identify which BDEW release window was active.
189 release: Option<crate::release::Release>,
190 /// The AHB revision identifier (e.g. `"3.2e"`, `"2.0h"`).
191 ///
192 /// May carry a correction letter that differs from `release` when BDEW issues
193 /// an AHB correction without a MIG change (e.g. INSRPT wire `1.1a` but AHB `1.1g`).
194 /// Including this in audit records disambiguates which rule set was applied.
195 ahb_revision: Option<&'static str>,
196 /// The parsed interchange envelope header (UNB fields), when the message was
197 /// validated from a full interchange (UNB…UNZ).
198 ///
199 /// `None` for bare messages (UNH…UNT only, no interchange wrapper). Present
200 /// when an interchange wrapper was detected and successfully validated by
201 /// `edifact_rs::validate_envelope_owned`.
202 ///
203 /// Combines with the validation issues to provide a single comprehensive
204 /// audit record (who sent it, when, control reference, and whether it was valid).
205 pub interchange_header: Option<crate::interchange::InterchangeHeader>,
206}
207
208impl EdiEnergyReport {
209 /// Construct from a raw [`ValidationReport`].
210 #[must_use]
211 #[allow(dead_code)] // used by feature-gated message modules
212 pub(crate) fn new(inner: ValidationReport) -> Self {
213 Self {
214 inner,
215 pruefidentifikator: None,
216 release: None,
217 ahb_revision: None,
218 interchange_header: None,
219 }
220 }
221
222 /// Construct with an associated Prüfidentifikator.
223 ///
224 /// Use this from message validation code that knows which PID-specific rule
225 /// pack was applied, so the PID is available in serialized issue summaries.
226 #[must_use]
227 #[allow(dead_code)] // used by feature-gated message modules
228 pub(crate) fn new_with_pid(inner: ValidationReport, pid: Option<u32>) -> Self {
229 Self {
230 inner,
231 pruefidentifikator: pid,
232 release: None,
233 ahb_revision: None,
234 interchange_header: None,
235 }
236 }
237
238 /// Attach profile metadata for unambiguous audit-log serialization.
239 ///
240 /// Sets `release` (wire format release code) and `ahb_revision` on the report.
241 /// Both are emitted in the serialized JSON output so downstream audit systems
242 /// can identify exactly which BDEW specification version governed the validation,
243 /// including AHB correction revisions that share a wire code (see.
244 #[must_use]
245 #[allow(dead_code)]
246 pub(crate) fn with_profile_meta(
247 mut self,
248 release: crate::release::Release,
249 ahb_revision: Option<&'static str>,
250 ) -> Self {
251 self.release = Some(release);
252 self.ahb_revision = ahb_revision;
253 self
254 }
255
256 /// Attach the interchange envelope header to the report.
257 ///
258 /// Call this when the message was validated from a full interchange (UNB…UNZ)
259 /// so that the report carries both routing metadata and validation findings as
260 /// a single audit record.
261 #[cfg(any(
262 feature = "utilmd",
263 feature = "mscons",
264 feature = "aperak",
265 feature = "contrl",
266 feature = "invoic",
267 feature = "remadv",
268 feature = "orders",
269 feature = "iftsta",
270 feature = "insrpt",
271 feature = "reqote",
272 feature = "partin",
273 feature = "ordchg",
274 feature = "ordrsp",
275 feature = "quotes",
276 feature = "comdis",
277 feature = "pricat",
278 feature = "utilts",
279 ))]
280 #[must_use]
281 pub(crate) fn with_interchange_header(
282 mut self,
283 header: crate::interchange::InterchangeHeader,
284 ) -> Self {
285 self.interchange_header = Some(header);
286 self
287 }
288
289 /// Error-level issues. Any entry here means the message is non-conformant.
290 ///
291 /// In the underlying `edifact-rs` model, `Critical`-severity issues are also
292 /// stored here (both `Critical` and `Error` map to the same errors bucket).
293 /// Use [`criticals`][Self::criticals] to distinguish them when needed.
294 #[must_use]
295 pub fn errors(&self) -> &[ValidationIssue] {
296 self.inner.errors()
297 }
298
299 /// Critical-severity issues — a subset of [`errors`][Self::errors].
300 ///
301 /// `Critical` issues indicate a structural violation severe enough to abort
302 /// further validation (e.g. a malformed `UNH` envelope segment). They are
303 /// stored in the same bucket as `Error`-severity issues by `edifact-rs`, so
304 /// `is_valid()` already returns `false` when any `Critical` issue is present.
305 ///
306 /// This accessor filters `errors()` by `ValidationSeverity::Critical` and
307 /// is useful when you need to distinguish abort-level failures from regular
308 /// conformance errors in monitoring dashboards or audit logs.
309 pub fn criticals(&self) -> impl Iterator<Item = &ValidationIssue> {
310 use edifact_rs::ValidationSeverity;
311 self.inner
312 .errors()
313 .iter()
314 .filter(|i| i.severity == ValidationSeverity::Critical)
315 }
316
317 /// Warning-level issues. The message may still be processable.
318 #[must_use]
319 pub fn warnings(&self) -> &[ValidationIssue] {
320 self.inner.warnings()
321 }
322
323 /// Informational notes that do not affect validity.
324 #[must_use]
325 pub fn infos(&self) -> &[ValidationIssue] {
326 self.inner.infos()
327 }
328
329 /// Returns `true` if there are no error-level issues.
330 ///
331 /// Warnings and infos do not affect this result.
332 #[must_use]
333 pub fn is_valid(&self) -> bool {
334 self.inner.is_valid()
335 }
336
337 /// Returns `true` if there is at least one error-level issue.
338 #[must_use]
339 pub fn has_errors(&self) -> bool {
340 self.inner.has_errors()
341 }
342
343 /// Returns `true` if there is at least one warning-level issue.
344 #[must_use]
345 pub fn has_warnings(&self) -> bool {
346 self.inner.has_warnings()
347 }
348
349 /// Total number of issues across all severity levels.
350 #[must_use]
351 pub fn total_issues(&self) -> usize {
352 self.inner.total_issues()
353 }
354
355 /// Iterate over all issues in order: errors → warnings → infos.
356 pub fn iter_issues(&self) -> impl Iterator<Item = &ValidationIssue> {
357 self.inner.iter_issues()
358 }
359
360 /// Iterate over issues from a specific validation layer.
361 ///
362 /// Filters by the same `rule_origin` tag used in [`ValidationIssueSummary::rule_origin`]:
363 /// - `"parse"` — EDIFACT parse / tokenizer errors (L1)
364 /// - `"directory"` — directory structure checks (L2)
365 /// - `"mig"` — MIG structural rules (L3)
366 /// - `"ahb"` — AHB Bedingungsoperator rules (L4–L5)
367 /// - `"custom"` — caller-supplied `CustomRulePack` rules (L6)
368 ///
369 /// This allows monitoring dashboards to quickly separate "sender sent garbage"
370 /// (L1–L2) from "sender violated BDEW process rules" (L3–L5).
371 ///
372 /// # Example
373 ///
374 /// ```rust,no_run
375 /// use edi_energy::{EdiEnergyMessage, Platform};
376 /// let msg = Platform::with_all_profiles().parse(b"UNB+...").unwrap();
377 /// let report = msg.validate().unwrap();
378 /// let ahb_errors: Vec<_> = report.issues_by_origin("ahb").collect();
379 /// ```
380 pub fn issues_by_origin<'a>(
381 &'a self,
382 origin: &'a str,
383 ) -> impl Iterator<Item = &'a ValidationIssue> + 'a {
384 self.inner.iter_issues().filter(move |issue| {
385 issue.rule_id.as_deref().and_then(classify_rule_origin) == Some(origin)
386 })
387 }
388
389 /// Return a new report containing only issues whose rule identifier starts with `prefix`.
390 ///
391 /// This allocates a new report (O(n)). For read-only access prefer
392 /// [`issues_with_rule_prefix`][Self::issues_with_rule_prefix].
393 #[must_use]
394 pub fn filter_by_rule_prefix(&self, prefix: &str) -> Self {
395 Self {
396 inner: self.inner.filter_by_rule_prefix(prefix),
397 pruefidentifikator: self.pruefidentifikator,
398 release: self.release.clone(),
399 ahb_revision: self.ahb_revision,
400 interchange_header: self.interchange_header.clone(),
401 }
402 }
403
404 /// Iterate over issues whose rule identifier starts with `prefix` without cloning.
405 ///
406 /// This is the O(1)-allocation alternative to [`filter_by_rule_prefix`][Self::filter_by_rule_prefix].
407 pub fn issues_with_rule_prefix<'a>(
408 &'a self,
409 prefix: &'a str,
410 ) -> impl Iterator<Item = &'a ValidationIssue> + 'a {
411 self.inner
412 .iter_issues()
413 .filter(move |issue| issue.rule_id().is_some_and(|id| id.starts_with(prefix)))
414 }
415
416 /// Return a new report containing only issues with an exact rule identifier.
417 #[must_use]
418 pub fn filter_by_rule_id(&self, rule_id: &str) -> Self {
419 Self {
420 inner: self.inner.filter_by_rule_id(rule_id),
421 pruefidentifikator: self.pruefidentifikator,
422 release: self.release.clone(),
423 ahb_revision: self.ahb_revision,
424 interchange_header: self.interchange_header.clone(),
425 }
426 }
427
428 /// Iterate over all issues matching an exact profile/MIG rule identifier.
429 pub fn issues_for_rule_id<'a>(
430 &'a self,
431 rule_id: &'a str,
432 ) -> impl Iterator<Item = &'a ValidationIssue> + 'a {
433 self.inner.issues_for_rule_id(rule_id)
434 }
435
436 /// A stable, deterministic text rendering suitable for snapshot tests and logs.
437 #[must_use]
438 pub fn render_deterministic(&self) -> String {
439 self.inner.render_deterministic()
440 }
441
442 /// Convert to a `Result`, consuming `self`.
443 ///
444 /// Returns `Ok(())` when there are no errors, `Err(report)` otherwise.
445 /// Warnings and infos are preserved in the `Err` variant.
446 ///
447 /// # Errors
448 ///
449 /// Returns `Err(self)` when the report contains one or more error-level issues.
450 pub fn into_result(self) -> Result<(), Self> {
451 if self.inner.has_errors() {
452 Err(self)
453 } else {
454 Ok(())
455 }
456 }
457
458 /// Convert to a library `Result`, consuming `self`.
459 ///
460 /// Returns `Ok(())` when there are no errors, `Err(Error::Validation { ... })`
461 /// otherwise. Use this when you want to propagate validation failure as a
462 /// first-class [`crate::Error`] variant rather than handling the raw report.
463 ///
464 /// Returns `Ok(self)` when the report has no error-level issues, `Err`
465 /// otherwise.
466 ///
467 /// This is the idiomatic alternative to [`into_error_result`][Self::into_error_result]:
468 /// callers that need the report for further inspection after an error can
469 /// recover it from the `Err` variant via pattern matching on
470 /// [`crate::Error::Validation`].
471 ///
472 /// # Errors
473 ///
474 /// Returns [`crate::Error::Validation`] when the report contains at least
475 /// one error-level issue.
476 pub fn as_result(self) -> Result<Self, crate::Error> {
477 if self.inner.has_errors() {
478 let count = self.inner.errors().len();
479 Err(crate::Error::Validation {
480 count,
481 report: self,
482 })
483 } else {
484 Ok(self)
485 }
486 }
487
488 /// Returns `Ok(())` when the report has no error-level issues, `Err` otherwise.
489 ///
490 /// Prefer [`as_result`][Self::as_result] when you need access to the report
491 /// after a validation failure.
492 ///
493 /// # Errors
494 ///
495 /// Returns [`crate::Error::Validation`] when the report contains at least one
496 /// error-level issue.
497 pub fn into_error_result(self) -> Result<(), crate::Error> {
498 if self.inner.has_errors() {
499 let count = self.inner.errors().len();
500 Err(crate::Error::Validation {
501 count,
502 report: self,
503 })
504 } else {
505 Ok(())
506 }
507 }
508
509 /// Convert to `Result<Self, Self>`, consuming `self`.
510 ///
511 /// Returns `Ok(self)` when valid (no errors), `Err(self)` when invalid.
512 /// Mirrors the `edifact_rs::ValidationReport::result()` API so call-sites
513 /// that use `?` propagation work symmetrically across both types.
514 ///
515 /// # Errors
516 ///
517 /// Returns `Err(self)` when the report contains at least one error-level issue.
518 #[must_use = "call into_result() or as_result() if you only need the side-effect"]
519 pub fn result(self) -> Result<Self, Self> {
520 if self.inner.has_errors() {
521 Err(self)
522 } else {
523 Ok(self)
524 }
525 }
526
527 /// Consume the wrapper and return the underlying [`ValidationReport`].
528 #[must_use]
529 pub fn into_inner(self) -> ValidationReport {
530 self.inner
531 }
532
533 /// The Prüfidentifikator the message was validated against, if known.
534 ///
535 /// Returns `None` when validation was performed without a PID-specific rule
536 /// pack (e.g. layer 1–3 structural checks only).
537 #[must_use]
538 pub fn pruefidentifikator(&self) -> Option<u32> {
539 self.pruefidentifikator
540 }
541
542 /// The wire-format release code from the profile used during validation.
543 ///
544 /// Returns `None` when the profile was not resolved through the registry
545 /// (e.g. when calling `validate_against` with an unknown release).
546 #[must_use]
547 pub fn release(&self) -> Option<&crate::release::Release> {
548 self.release.as_ref()
549 }
550
551 /// The AHB revision identifier (e.g. `"3.2e"`, `"2.0h"`).
552 ///
553 /// May differ from the wire release code when BDEW publishes an AHB
554 /// correction without a MIG change. `None` when not tracked for this profile.
555 #[must_use]
556 pub fn ahb_revision(&self) -> Option<&'static str> {
557 self.ahb_revision
558 }
559
560 /// Merge all issues from `other` into `self`.
561 ///
562 /// Useful when running multiple independent validation pipelines and
563 /// combining their results into a single report.
564 ///
565 /// The PID of `self` is preserved; `other.pruefidentifikator` is ignored.
566 pub fn merge(&mut self, other: Self) {
567 self.inner.merge(other.inner);
568 }
569}
570
571impl fmt::Display for EdiEnergyReport {
572 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573 write!(
574 f,
575 "{} error(s), {} warning(s), {} info(s)",
576 self.inner.errors().len(),
577 self.inner.warnings().len(),
578 self.inner.infos().len()
579 )
580 }
581}
582
583#[cfg(feature = "serde")]
584impl serde::Serialize for EdiEnergyReport {
585 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
586 use serde::ser::SerializeStruct;
587 let errors = self.inner.errors();
588 let warnings = self.inner.warnings();
589 let infos = self.inner.infos();
590 let pid = self.pruefidentifikator;
591 // Count optional top-level fields to size the struct correctly.
592 let opt_field_count = usize::from(pid.is_some())
593 + usize::from(self.release.is_some())
594 + usize::from(self.ahb_revision.is_some())
595 + usize::from(self.interchange_header.is_some());
596 let mut st = s.serialize_struct("EdiEnergyReport", 5 + opt_field_count)?;
597 st.serialize_field("valid", &self.inner.is_valid())?;
598 st.serialize_field(
599 "errors",
600 &errors
601 .iter()
602 .map(|i| ValidationIssueSummary::from_issue_with_pid(i, pid))
603 .collect::<Vec<_>>(),
604 )?;
605 st.serialize_field(
606 "warnings",
607 &warnings
608 .iter()
609 .map(|i| ValidationIssueSummary::from_issue_with_pid(i, pid))
610 .collect::<Vec<_>>(),
611 )?;
612 st.serialize_field(
613 "infos",
614 &infos
615 .iter()
616 .map(|i| ValidationIssueSummary::from_issue_with_pid(i, pid))
617 .collect::<Vec<_>>(),
618 )?;
619 st.serialize_field(
620 "totalIssues",
621 &(errors.len() + warnings.len() + infos.len()),
622 )?;
623 if let Some(pid) = pid {
624 st.serialize_field("pruefidentifikator", &pid)?;
625 }
626 if let Some(ref rel) = self.release {
627 st.serialize_field("release", rel.as_str())?;
628 }
629 if let Some(rev) = self.ahb_revision {
630 st.serialize_field("ahbRevision", rev)?;
631 }
632 if let Some(ref hdr) = self.interchange_header {
633 st.serialize_field("interchangeHeader", hdr)?;
634 }
635 st.end()
636 }
637}