Skip to main content

edi_energy/
lib.rs

1//! `edi-energy` — EDI@Energy EDIFACT parser and validator for the German energy market.
2//!
3//! # Quick start
4//!
5//! ```rust,no_run
6//! use edi_energy::{Platform, AnyMessage, EdiEnergyMessage};
7//!
8//! let input: &[u8] = b"UNB+UNOA:3+SENDER+RECEIVER+200101:0900+1'UNH+1+UTILMD:D:11A:UN:S2.2'BGM+E01+11001+9'UNT+2+1'UNZ+1+1'";
9//! let msg = Platform::with_all_profiles().parse(input)?;
10//! let report = msg.validate()?;
11//! assert!(report.is_valid());
12//! # Ok::<(), edi_energy::Error>(())
13//! ```
14//!
15//! # Supported releases
16//!
17//! This crate ships built-in profiles for the **S-track** (Strom) and
18//! **G-track** (Gas) UTILMD releases introduced by the 2024 format split:
19//! `S2.1`, `S2.2` (Strom) and `G1.1`, `G1.2` (Gas).
20//!
21//! **Classic UTILMD releases (5.5.x)** — messages with wire release codes such
22//! as `5.5.3a`, `5.5.4a`, `5.5.5a`, `5.5.6a`, `5.5.7a`, or `5.5.8a` — can be
23//! **parsed** (the EDIFACT segment structure is read and typed fields extracted)
24//! but **cannot be validated**: [`AnyMessage::validate`] and
25//! [`AnyMessage::validate_against`] return [`Error::ProfileNotFound`] for any
26//! classic-track release because no 5.5.x MIG/AHB profiles are bundled.
27//!
28//! If you need validation support for classic-track archive messages, build
29//! and register custom [`registry::Profile`] implementations for those releases.
30
31#![deny(unsafe_code)]
32#![deny(clippy::undocumented_unsafe_blocks)]
33#![deny(missing_docs)]
34#![warn(clippy::pedantic)]
35#![allow(clippy::module_name_repetitions)]
36// `Error::Validation` intentionally carries a full `EdiEnergyReport` for rich diagnostics.
37// Boxing it would change the public pattern-matching API and is not worth the churn.
38#![allow(clippy::result_large_err)]
39// Generated profile code uses patterns that trigger bulk lint noise.
40// These are correct and idiomatic for machine-generated output.
41#![allow(clippy::unnecessary_map_or)]
42#![allow(clippy::collapsible_if)]
43#![allow(clippy::manual_contains)]
44#![allow(clippy::unnested_or_patterns)]
45#![allow(clippy::match_single_binding)]
46#![allow(clippy::redundant_closure_for_method_calls)]
47// Generated profile helper functions (e.g. `fn ahb_xxx_pack()`) have obvious
48// must-use semantics; suppress this pedantic lint for the whole crate.
49#![allow(clippy::must_use_candidate)]
50// Builder methods that return `Self` all have obvious must-use semantics.
51#![allow(clippy::return_self_not_must_use)]
52
53mod agency_code;
54mod any_message;
55mod custom_rule_pack;
56mod error;
57mod interchange;
58mod light_message;
59mod message;
60mod message_type;
61mod object_type;
62mod parse;
63mod platform;
64mod pruefidentifikator;
65mod release;
66mod report;
67
68/// Fluent builder APIs for constructing EDI@Energy messages.
69pub mod builders;
70/// Concrete EDI@Energy message type structs, one sub-module per message type.
71pub mod messages;
72/// Profile registry mapping `(MessageType, Release)` pairs to validation rules.
73pub mod registry;
74
75#[doc(hidden)]
76pub(crate) mod generated;
77
78pub use agency_code::AgencyCode;
79pub use any_message::AnyMessage;
80pub use custom_rule_pack::CustomRulePack;
81pub use error::{Error, ProfileError};
82pub use interchange::{InterchangeHeader, MessageEnvelope, ParsedInterchange, ReceiptContext};
83pub use light_message::LightMessage;
84pub use message::EdiEnergyMessage;
85pub use message_type::MessageType;
86pub use object_type::ObjectType;
87pub use parse::{
88    DEFAULT_MAX_SEGMENT_BYTES, InterchangeIter, ParseConfig, Parser, parse, parse_envelope_only,
89    parse_interchange,
90};
91pub use platform::Platform;
92pub use pruefidentifikator::Pruefidentifikator;
93pub use registry::{ProcessContext, ReleaseRegistry, TRANSITION_GRACE_DAYS, TransitionState};
94pub use release::{Release, ReleaseKind, ReleaseTrack};
95pub use report::EdiEnergyReport;
96
97/// Well-known release identifiers for all registered profiles.
98///
99/// Use these instead of `Release::new("...")` to get a compile error when a
100/// profile is removed or renamed after a BDEW format update.
101pub mod releases {
102    #[cfg(any(
103        feature = "aperak",
104        feature = "comdis",
105        feature = "contrl",
106        feature = "iftsta",
107        feature = "insrpt",
108        feature = "invoic",
109        feature = "mscons",
110        feature = "ordchg",
111        feature = "orders",
112        feature = "ordrsp",
113        feature = "partin",
114        feature = "pricat",
115        feature = "quotes",
116        feature = "remadv",
117        feature = "reqote",
118        feature = "utilmd",
119        feature = "utilts",
120    ))]
121    pub use crate::generated::releases::*;
122}
123
124// Re-export edifact-rs types users may need
125pub use edifact_rs::{
126    EdifactDeserialize, EdifactSerialize, ReaderConfig, ValidationIssue, ValidationReport,
127    ValidationSeverity,
128};
129// ValidationIssueSummary is unconditionally available.
130// serde::Serialize is available on the type when the `serde` feature is enabled.
131pub use report::ValidationIssueSummary;
132
133// Re-export typed hierarchy structs (segment groups) for each message type.
134#[cfg(feature = "aperak")]
135pub use messages::aperak::AperakError;
136#[cfg(feature = "contrl")]
137pub use messages::contrl::{ContrlElementError, ContrlMessageResponse, ContrlSegmentError};
138#[cfg(feature = "mscons")]
139pub use messages::mscons::{
140    MsconsDeliveryPoint, MsconsLineItem, MsconsQuantity, MsconsReference, MsconsTimeSeries,
141};
142#[cfg(feature = "utilmd")]
143pub use messages::utilmd::{UtilmdReference, UtilmdTransaction};
144
145/// Validate `msg` and additionally enforce that its Prüfidentifikator matches
146/// `expected`.
147///
148/// This is the free-function replacement for the former
149/// `EdiEnergyMessage::validate_pruefidentifikator` trait method.  Splitting the
150/// concern out of the trait reduces boilerplate in every implementation and
151/// keeps the trait surface minimal.
152///
153/// Behaviour:
154/// - Calls [`EdiEnergyMessage::validate`] to obtain the standard validation
155///   report (all layers L1–L5).
156/// - If [`EdiEnergyMessage::detect_pruefidentifikator`] returns `Ok(pid)` and
157///   `pid == expected`, the report is returned unchanged.
158/// - If the detected PID does not match `expected`, or if no PID can be
159///   detected, a rule-`EE-PID-001` error is appended to the report and the
160///   (now-invalid) report is returned.
161///
162/// # Errors
163///
164/// Returns `Err` only when [`EdiEnergyMessage::validate`] itself fails (e.g.
165/// parse failure, profile not registered).  A PID mismatch is always surfaced
166/// as a validation issue inside the returned `Ok(EdiEnergyReport)`, not as an
167/// `Err`.
168#[must_use = "validation result must be checked for errors"]
169pub fn validate_and_check_pid(
170    msg: &impl EdiEnergyMessage,
171    expected: Pruefidentifikator,
172) -> Result<EdiEnergyReport, Error> {
173    let report = msg.validate()?;
174    match msg.detect_pruefidentifikator() {
175        Ok(actual) if actual == expected => Ok(report),
176        Ok(actual) => {
177            let mut inner = report.into_inner();
178            inner.add_error(
179                edifact_rs::ValidationIssue::new(
180                    edifact_rs::ValidationSeverity::Error,
181                    format!("expected Pruefidentifikator {expected}, found {actual}"),
182                )
183                .with_rule_id("EE-PID-001")
184                .with_segment("BGM"),
185            );
186            Ok(EdiEnergyReport::new(inner))
187        }
188        Err(_) => {
189            let mut inner = report.into_inner();
190            inner.add_error(
191                edifact_rs::ValidationIssue::new(
192                    edifact_rs::ValidationSeverity::Error,
193                    format!("expected Pruefidentifikator {expected}, but none was found"),
194                )
195                .with_rule_id("EE-PID-001")
196                .with_segment("BGM"),
197            );
198            Ok(EdiEnergyReport::new(inner))
199        }
200    }
201}