Skip to main content

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}