edi_energy/light_message.rs
1/// Cheap envelope-only view of a parsed EDIFACT message.
2///
3/// `LightMessage` holds the raw `Vec<OwnedSegment>` plus the small set of
4/// fields that any AS4 router or forwarder needs:
5///
6/// - Message type code (e.g. `"UTILMD"`)
7/// - Association assigned code / release (e.g. `"S2.1"`)
8/// - UNH message reference
9/// - Prüfidentifikator (when detectable from UNH/BGM/RFF without full parse)
10///
11/// Typed field extraction (the `Vec<Dtm>`, `Vec<Nad>`, etc. on concrete message
12/// structs) is **not** performed. For a MSCONS message with 1 000 delivery-point
13/// groups, this avoids O(n) heap allocations on the routing hot path.
14///
15/// # Routing pattern
16///
17/// ```rust,no_run
18/// use edi_energy::{parse_envelope_only, EdiEnergyMessage};
19///
20/// let bytes = b"UNB+...";
21/// let light = parse_envelope_only(bytes)?;
22/// println!("type : {}", light.message_type_code());
23/// println!("release: {}", light.assoc_code());
24///
25/// // Only pay the full-parse cost when you actually need typed access:
26/// if light.message_type_code() == "UTILMD" {
27/// let msg = light.into_message()?;
28/// let report = msg.validate()?;
29/// }
30/// # Ok::<(), edi_energy::Error>(())
31/// ```
32use edifact_rs::OwnedSegment;
33
34use crate::{AnyMessage, Error, MessageType, Pruefidentifikator, Release};
35
36/// Envelope-only view of a parsed EDIFACT/EDI@Energy message.
37///
38/// See the module-level docs for a full description and the routing
39/// pattern.
40#[derive(Debug)]
41pub struct LightMessage {
42 /// All parsed segments (owned). Used by [`into_message`](Self::into_message)
43 /// to avoid re-parsing when the caller decides to upgrade to a full message.
44 pub(crate) segments: Vec<OwnedSegment>,
45 /// UNH DE 0065 — EDIFACT message type code (e.g. `"UTILMD"`, `"MSCONS"`).
46 message_type_code: Box<str>,
47 /// UNH S009 DE 0057 — association assigned code (e.g. `"S2.1"`, `"2.4c"`).
48 assoc_code: Box<str>,
49 /// UNH DE 0062 — message reference identifier.
50 message_ref: Box<str>,
51 /// BGM DE 1004 or RFF+Z13 — Prüfidentifikator, if detectable.
52 pruefidentifikator: Option<u32>,
53}
54
55impl LightMessage {
56 /// Construct from a parsed segment list and a registry reference.
57 ///
58 /// Extracts the UNH fields and PID without building any typed message struct.
59 pub(crate) fn from_segments(
60 segments: Vec<OwnedSegment>,
61 registry: &crate::registry::ReleaseRegistry,
62 ) -> Result<Self, Error> {
63 let (message_ref, message_type_code, assoc_code) = {
64 let unh = segments
65 .iter()
66 .find(|s| s.tag == "UNH")
67 .ok_or(Error::MissingSegment("UNH"))?;
68 let message_ref = unh.element_str(0).unwrap_or_default().to_owned();
69 let message_type_code = unh
70 .component_str(1, 0)
71 .ok_or(Error::MalformedSegment("UNH"))?
72 .to_owned();
73 let assoc_code = unh.component_str(1, 4).unwrap_or_default().to_owned();
74 (message_ref, message_type_code, assoc_code)
75 };
76
77 // Look up PID source from registry (same logic as full parse) so we can
78 // surface the PID without constructing any typed struct.
79 let pruefidentifikator: Option<u32> =
80 match crate::parse::resolve_pid_source_pub(&message_type_code, &assoc_code, registry) {
81 crate::registry::PidSource::RffZ13 => segments
82 .iter()
83 .find(|s| s.tag == "RFF" && s.element_str(0).is_some_and(|q| q == "Z13"))
84 .and_then(|rff| rff.component_str(0, 1))
85 .and_then(|s| s.parse().ok()),
86 crate::registry::PidSource::BgmDe1004 => segments
87 .iter()
88 .find(|s| s.tag == "BGM")
89 .and_then(|bgm| bgm.element_str(1))
90 .and_then(|s| s.parse().ok()),
91 };
92
93 Ok(Self {
94 segments,
95 message_type_code: message_type_code.into_boxed_str(),
96 assoc_code: assoc_code.into_boxed_str(),
97 message_ref: message_ref.into_boxed_str(),
98 pruefidentifikator,
99 })
100 }
101
102 // ── Accessors ─────────────────────────────────────────────────────────────
103
104 /// Raw EDIFACT message type code from UNH S009 DE 0065 (e.g. `"UTILMD"`).
105 #[must_use]
106 pub fn message_type_code(&self) -> &str {
107 &self.message_type_code
108 }
109
110 /// Parsed [`MessageType`] discriminant, or `None` for unknown types.
111 #[must_use]
112 pub fn try_message_type(&self) -> Option<MessageType> {
113 MessageType::from_unh_code(&self.message_type_code)
114 }
115
116 /// Association assigned code from UNH S009 DE 0057 (e.g. `"S2.1"`, `"2.4c"`).
117 #[must_use]
118 pub fn assoc_code(&self) -> &str {
119 &self.assoc_code
120 }
121
122 /// Parsed [`Release`] derived from [`assoc_code`](Self::assoc_code).
123 #[must_use]
124 pub fn release(&self) -> Release {
125 Release::new(&self.assoc_code)
126 }
127
128 /// UNH message reference (DE 0062).
129 #[must_use]
130 pub fn message_ref(&self) -> &str {
131 &self.message_ref
132 }
133
134 /// Prüfidentifikator extracted from BGM or RFF+Z13, if present.
135 #[must_use]
136 pub fn pruefidentifikator(&self) -> Option<Pruefidentifikator> {
137 self.pruefidentifikator
138 .and_then(|n| Pruefidentifikator::new(n).ok())
139 }
140
141 /// Raw segment slice. Available for advanced callers that need to inspect
142 /// specific segments without upgrading to a full [`AnyMessage`].
143 #[must_use]
144 pub fn segments(&self) -> &[OwnedSegment] {
145 &self.segments
146 }
147
148 // ── Upgrade ───────────────────────────────────────────────────────────────
149
150 /// Upgrade to a fully typed [`AnyMessage`], performing typed field extraction.
151 ///
152 /// The owned segment buffer is moved, so no re-allocation is required. The
153 /// additional cost is the typed field extraction pass (O(segments) work),
154 /// which is what routing-only paths avoid by holding a `LightMessage`.
155 ///
156 /// # Errors
157 ///
158 /// Returns `Err` when the message type is compiled out (`FeatureNotEnabled`)
159 /// or the UNH segment is malformed.
160 pub fn into_message(self) -> Result<AnyMessage, Error> {
161 crate::parse::dispatch_message(self.segments, crate::registry::ReleaseRegistry::global())
162 }
163}