Skip to main content

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}