Skip to main content

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}