edi_energy/message.rs
1use edifact_rs::OwnedSegment;
2
3use crate::{CustomRulePack, EdiEnergyReport, Error, MessageType, Pruefidentifikator, Release};
4
5/// Core abstraction for all EDI@Energy message types.
6///
7/// Every concrete message type (`UtilmdMessage`, `MsconsMessage`, …) implements
8/// this trait, and [`AnyMessage`](crate::AnyMessage) delegates to it via dynamic dispatch or
9/// exhaustive matching.
10///
11/// The trait is object-safe: all methods return owned values or `Result<Owned, Error>`.
12///
13/// ## Dual-representation design
14///
15/// Each parsed message holds **two** views of the same data:
16///
17/// 1. **Raw segments** (`Vec<OwnedSegment>`) — the authoritative wire representation.
18/// This is what [`serialize`](EdiEnergyMessage::serialize) serialises and what
19/// [`validate`](EdiEnergyMessage::validate) runs against.
20///
21/// 2. **Typed fields** (e.g. `bgm`, `nad`, `dtm` on concrete structs) — pre-extracted
22/// convenience views populated at parse time. These are read-only helpers for
23/// field access patterns like routing and logging.
24///
25/// **Mutations to typed fields are silently discarded on `serialize`.** If you need
26/// to modify a message before re-sending it, use the builder API in
27/// [`crate::builders`] to construct a new message from scratch, or manipulate the
28/// raw segment bytes directly.
29pub trait EdiEnergyMessage: Send + Sync {
30 /// Returns the message-type discriminant, or `None` for unrecognised
31 /// message types (i.e. [`AnyMessage::Unknown`](crate::AnyMessage)).
32 ///
33 /// This is the primary required method for message-type identification.
34 /// Concrete message types (e.g. `UtilmdMessage`) always return `Some(…)`.
35 #[must_use]
36 fn try_message_type(&self) -> Option<MessageType>;
37
38 /// Extracts the EDI@Energy release identifier from the UNH S009 composite (DE 0057).
39 ///
40 /// Returns `Err(Error::MissingRelease)` when the field is absent or empty.
41 ///
42 /// The returned reference borrows from the message; no allocation is performed.
43 ///
44 /// # Errors
45 ///
46 /// Returns [`Error::MissingRelease`] when the UNH S009 association code (DE 0057) is
47 /// absent or empty.
48 fn detect_release(&self) -> Result<&Release, Error>;
49
50 /// Returns the UNH message reference identifier (DE 0062).
51 ///
52 /// This is the sender-assigned reference string that correlates UNH/UNT pairs
53 /// and is mirrored in acknowledgement messages (APERAK, CONTRL).
54 fn message_ref(&self) -> &str;
55
56 /// Extracts the Pruefidentifikator from the BGM document-identifier field (DE 1004).
57 ///
58 /// Returns `Err(Error::MissingPruefidentifikator)` when the BGM segment is absent,
59 /// or `Err(Error::InvalidPruefidentifikator)` when the value is outside 10000–99999.
60 ///
61 /// # Errors
62 ///
63 /// - [`Error::MissingPruefidentifikator`] — BGM segment absent or DE 1004 empty.
64 /// - [`Error::InvalidPruefidentifikatorRange`] — value is outside the range 10000–99999.
65 /// - [`Error::InvalidPruefidentifikatorFormat`] — value is not a valid integer.
66 fn detect_pruefidentifikator(&self) -> Result<Pruefidentifikator, Error>;
67
68 /// Validate the message using the profile registered for its detected release.
69 ///
70 /// Performs all applicable validation layers (1–5) for which profile data is available.
71 /// Returns the full [`EdiEnergyReport`]; use [`EdiEnergyReport::is_valid`] to check
72 /// pass/fail, or `.into_result()` to propagate errors.
73 ///
74 /// # Errors
75 ///
76 /// Returns `Err` only when validation itself cannot run (e.g. parse failure,
77 /// profile not registered). Validation findings are carried in [`EdiEnergyReport`].
78 #[must_use = "validation result must be checked for errors"]
79 fn validate(&self) -> Result<EdiEnergyReport, Error>;
80
81 /// Validate against an explicit release, overriding the detected one.
82 ///
83 /// Useful for strict conformance testing or when the release code is absent.
84 ///
85 /// # Unknown message types
86 ///
87 /// For [`AnyMessage::Unknown`](crate::AnyMessage), this method returns
88 /// `Ok(report)` where `report.is_valid() == true` and contains a single
89 /// Warning with rule ID `"UNKNOWN-MSG-TYPE"`. This allows interchanges
90 /// with mixed message types to validate without failing on unrecognised
91 /// types. The `release` parameter is not used in this case.
92 ///
93 /// # Errors
94 ///
95 /// Returns `Err(Error::ProfileNotFound)` when no profile is registered for
96 /// the given `(message_type, release)` pair.
97 #[must_use = "validation result must be checked for errors"]
98 fn validate_against(&self, release: &Release) -> Result<EdiEnergyReport, Error>;
99
100 /// Validate and merge an additional caller-supplied rule pack on top of all
101 /// built-in validation layers (L1–L5).
102 ///
103 /// The `extra` pack runs after the standard semantic rules and can be used
104 /// for application-level business rules, regulatory additions, or test-time
105 /// strictness escalation — without needing to fork the message type.
106 ///
107 /// Use [`CustomRulePack`](crate::CustomRulePack) to construct the rule pack
108 /// without a direct dependency on `edifact-rs`.
109 ///
110 /// # Errors
111 ///
112 /// Same as [`validate`](Self::validate).
113 #[must_use = "validation result must be checked for errors"]
114 fn validate_with_pack(&self, extra: CustomRulePack) -> Result<EdiEnergyReport, Error>;
115
116 /// Validate the message for the normative date encoded in `ctx`.
117 ///
118 /// This is the primary entry point for AS4 adapter integration:
119 ///
120 /// - Checks that the message's declared release is normatively acceptable on
121 /// `ctx`'s date (taking the 7-day `TRANSITION_GRACE_DAYS` window into account).
122 /// If the release is outside the acceptable window, returns
123 /// `Err(Error::ProfileNotFound)`.
124 /// - Validates against the sender's declared release on `ctx`'s date. This
125 /// preserves the sender's conformance claim: a message in the outgoing format
126 /// during the transition window is validated against the outgoing profile, not
127 /// the incoming one.
128 ///
129 /// # Date threading
130 ///
131 /// Both the `is_acceptable` check and the profile lookup use `ctx.date()` as
132 /// the reference date — no call to `now_utc()` is made. This ensures the
133 /// method is fully deterministic for tests that set an explicit reference date
134 /// (resolves previously `validate_against` used `now_utc()` internally,
135 /// causing an off-by-one risk near midnight and making date-deterministic
136 /// integration tests unreliable).
137 ///
138 /// # Transition handling
139 ///
140 /// During the 7-day grace window both outgoing and incoming releases are
141 /// acceptable (`is_acceptable` returns `true` for both). A receiver must
142 /// accept messages in either format during this period. `validate_with_context`
143 /// respects this by checking `is_acceptable` first and then running validation
144 /// only against the sender's declared release — callers do not need to implement
145 /// the `TransitionState` dispatch manually.
146 ///
147 /// # Errors
148 ///
149 /// Returns `Err(Error::MissingRelease)` when the message has no release code.
150 ///
151 /// Returns `Err(Error::ProfileNotFound)` when the message's release is not
152 /// normatively acceptable on `ctx`'s date (outside the valid + grace window).
153 ///
154 /// Other errors mirror those of [`validate_against`](Self::validate_against).
155 #[must_use = "validation result must be checked for errors"]
156 fn validate_with_context(
157 &self,
158 ctx: &crate::registry::ProcessContext,
159 ) -> Result<EdiEnergyReport, Error> {
160 let release = self.detect_release()?;
161 // Unknown message types have no typed MessageType and therefore cannot be
162 // checked against a ProcessContext. Fall through to validate_on_date,
163 // which returns a warning report for Unknown variants.
164 let Some(mt) = self.try_message_type() else {
165 return self.validate_on_date(ctx.date());
166 };
167 if !ctx.is_acceptable(mt, release) {
168 return Err(Error::ProfileNotFound {
169 message_type: mt,
170 release: release.clone(),
171 });
172 }
173 // Use ctx.date() (not now_utc()) so profile lookup is deterministic for
174 // date-sensitive tests and near-midnight race conditions are eliminated.
175 self.validate_on_date(ctx.date())
176 }
177
178 /// Validate the message as if today's date were `reference_date`.
179 ///
180 /// Equivalent to [`validate`](Self::validate) but uses `reference_date` for
181 /// profile validity lookups instead of `time::OffsetDateTime::now_utc()`.
182 /// This is the recommended way to write deterministic tests that exercise
183 /// profile-version disambiguation without depending on the wall clock.
184 ///
185 /// # Example
186 /// ```rust,ignore
187 /// let date = time::Date::from_calendar_date(2026, time::Month::January, 15).unwrap();
188 /// let report = message.validate_on_date(date)?;
189 /// ```
190 ///
191 /// # Errors
192 ///
193 /// Same as [`validate`](Self::validate).
194 #[must_use = "validation result must be checked for errors"]
195 fn validate_on_date(&self, reference_date: time::Date) -> Result<EdiEnergyReport, Error>;
196
197 /// Serialize the message back to EDIFACT wire bytes.
198 ///
199 /// The returned bytes are a valid EDIFACT document and can be re-parsed.
200 ///
201 /// **Serialization uses the raw segment list, not the typed fields.**
202 /// Mutations to typed fields (e.g. `msg.bgm`, `msg.nad`) are not reflected
203 /// in the output. To modify a message before re-sending, use the builder
204 /// API in [`crate::builders`] instead.
205 ///
206 /// # Errors
207 ///
208 /// Returns `Err(Error::Serialize(_))` when the underlying EDIFACT serializer
209 /// cannot encode the segment data. In practice this only occurs when segment
210 /// content contains characters that are not valid in the EDIFACT character set
211 /// (e.g. raw control bytes). For messages produced by this crate's parsers
212 /// (which have already validated input bytes) `serialize()` is effectively
213 /// infallible; for messages constructed by mutating raw segments directly the
214 /// caller should handle the error path.
215 fn serialize(&self) -> Result<Vec<u8>, Error>;
216
217 /// Returns the raw parsed segments (UNH … UNT inclusive).
218 ///
219 /// This slice is the authoritative source for serialization and validation.
220 /// Typed fields on concrete message structs are derived views; mutations to
221 /// those fields do **not** affect the segment list.
222 fn segments(&self) -> &[OwnedSegment];
223}