Skip to main content

edifact_rs/
writer.rs

1//! EDIFACT writer — serializes [`Segment`]s to wire format.
2
3use crate::{error::EdifactError, model::Segment, tokenizer::ServiceStringAdvice};
4use std::borrow::Cow;
5use std::io::Write;
6
7/// Streaming EDIFACT writer.
8///
9/// Wraps any [`Write`] implementation and serializes segments one at a time.
10/// Call [`Writer::finish`] to flush and get the underlying writer back.
11pub struct Writer<W: Write> {
12    inner: W,
13    ssa: ServiceStringAdvice,
14    /// Running count of segments written.  `u64` to prevent silent overflow on
15    /// pathological inputs (a `u32` would wrap after ~4 billion segments).
16    segment_count: u64,
17}
18
19impl<W: Write> Writer<W> {
20    /// Create a new writer with default EDIFACT delimiters.
21    pub fn new(inner: W) -> Self {
22        Self {
23            inner,
24            ssa: ServiceStringAdvice::default(),
25            segment_count: 0,
26        }
27    }
28
29    /// Create a writer with custom delimiters and write a UNA segment first.
30    pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
31        // All five active service characters must be mutually distinct, non-whitespace,
32        // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
33        if !ssa.is_valid() {
34            return Err(EdifactError::InvalidUna);
35        }
36        // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
37        let una = [
38            b'U',
39            b'N',
40            b'A',
41            ssa.component_sep,
42            ssa.element_sep,
43            ssa.decimal_mark,
44            ssa.release_char,
45            ssa.repetition_sep,
46            ssa.segment_term,
47        ];
48        inner.write_all(&una)?;
49        Ok(Self {
50            inner,
51            ssa,
52            segment_count: 0,
53        })
54    }
55
56    /// Write a single segment.
57    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
58        // Tag
59        self.inner.write_all(seg.tag.as_bytes())?;
60
61        for element in &seg.elements {
62            // Element separator
63            self.inner.write_all(&[self.ssa.element_sep])?;
64            let mut first_component = true;
65            for (component, _) in &element.components {
66                if !first_component {
67                    self.inner.write_all(&[self.ssa.component_sep])?;
68                }
69                first_component = false;
70                self.write_escaped(component)?;
71            }
72        }
73
74        // Segment terminator
75        self.inner.write_all(&[self.ssa.segment_term])?;
76        self.segment_count += 1;
77        Ok(())
78    }
79
80    /// Write a raw segment from tag + element string slices.
81    ///
82    /// Each element string is split on the **active component-separator byte** from the
83    /// configured [`ServiceStringAdvice`][crate::ServiceStringAdvice] to identify component
84    /// boundaries.  The default component separator is `:` (0x3A), but this can differ when a
85    /// non-default `UNA` string was used to construct the writer.
86    ///
87    /// # Delimiter dependency
88    ///
89    /// Callers that embed the literal `:` character in element strings rely on `:` being
90    /// the component separator.  When the writer uses a non-default delimiter set, `:` will
91    /// **not** be treated as a component boundary and the segment will be written incorrectly.
92    ///
93    /// **UTF-8 safety**: EDIFACT syntax requires all delimiter bytes to be single-byte ASCII
94    /// characters (values 0x00–0x7F).  Non-ASCII delimiter bytes would bisect multi-byte UTF-8
95    /// sequences in data values and produce malformed output.  All fields of
96    /// [`ServiceStringAdvice`][crate::ServiceStringAdvice] must therefore hold ASCII byte values.
97    ///
98    /// To produce correct output regardless of the active delimiter, prefer
99    /// [`Self::write_segment_parts`] which accepts pre-split component slices.
100    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
101        self.inner.write_all(tag.as_bytes())?;
102        let comp_sep = self.ssa.component_sep;
103        for el in elements {
104            self.inner.write_all(&[self.ssa.element_sep])?;
105            // Byte-level split: EDIFACT delimiters are always single bytes.
106            let mut parts = el.as_bytes().split(|&b| b == comp_sep);
107            if let Some(first) = parts.next() {
108                // INVARIANT: input is valid UTF-8 and we split on a single-byte ASCII
109                // delimiter, so each part remains a valid UTF-8 slice.
110                self.write_escaped(
111                    std::str::from_utf8(first).map_err(|_| EdifactError::InvalidUtf8)?,
112                )?;
113            }
114            for part in parts {
115                self.inner.write_all(&[comp_sep])?;
116                self.write_escaped(
117                    std::str::from_utf8(part).map_err(|_| EdifactError::InvalidUtf8)?,
118                )?;
119            }
120        }
121        self.inner.write_all(&[self.ssa.segment_term])?;
122        self.segment_count += 1;
123        Ok(())
124    }
125
126    /// Write a segment from a tag and pre-split element/component data.
127    ///
128    /// `elements` is a slice of elements; each element is a sequence of component strings.
129    /// This avoids the lifetime constraints of [`Self::write_segment`] when building
130    /// segments from runtime-owned data (e.g. inside [`crate::WriterEmitter`]).
131    pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
132    where
133        E: AsRef<[String]>,
134    {
135        self.inner.write_all(tag.as_bytes())?;
136        for element in elements {
137            self.inner.write_all(&[self.ssa.element_sep])?;
138            let mut first = true;
139            for comp in element.as_ref() {
140                if !first {
141                    self.inner.write_all(&[self.ssa.component_sep])?;
142                }
143                first = false;
144                self.write_escaped(comp.as_str())?;
145            }
146        }
147        self.inner.write_all(&[self.ssa.segment_term])?;
148        self.segment_count += 1;
149        Ok(())
150    }
151
152    /// Flush and return the underlying writer.
153    pub fn finish(mut self) -> Result<W, EdifactError> {
154        self.inner.flush()?;
155        Ok(self.inner)
156    }
157
158    /// Write the `UNT` segment and return the inner writer.
159    ///
160    /// The segment count written into `UNT` element 1 (DE 0074) is the number of
161    /// segments already written **plus one** for the `UNT` segment itself, which
162    /// EDIFACT requires to be included in the count alongside `UNH`.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if writing fails.  Do **not** call [`write_raw`][Self::write_raw] or
167    /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
168    pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
169        // DE 0074: count includes UNH and UNT themselves.
170        let count = self.segment_count + 1;
171        let count_str = count.to_string();
172        self.write_raw("UNT", &[count_str.as_str(), message_ref])?;
173        self.finish()
174    }
175
176    /// Returns the total number of segments written so far.
177    pub fn segment_count(&self) -> u64 {
178        self.segment_count
179    }
180
181    /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
182    pub fn service_string_advice(&self) -> ServiceStringAdvice {
183        self.ssa
184    }
185
186    /// Escape a value string for inclusion in an EDIFACT segment.
187    ///
188    /// Any character in `value` that matches the active element separator,
189    /// component separator, release character, or segment terminator is escaped
190    /// by prefixing it with the release character (default `?`).
191    ///
192    /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
193    /// avoiding an allocation on the fast path.
194    ///
195    /// # Example
196    ///
197    /// ```rust,ignore
198    /// let writer = Writer::new(std::io::sink());
199    /// // '+' must be escaped since it is the default element separator.
200    /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
201    /// ```
202    pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
203        let (elem, comp, release, term) = (
204            self.ssa.element_sep,
205            self.ssa.component_sep,
206            self.ssa.release_char,
207            self.ssa.segment_term,
208        );
209        let bytes = value.as_bytes();
210        let needs_escape = bytes
211            .iter()
212            .any(|&b| b == elem || b == comp || b == release || b == term);
213        if !needs_escape {
214            return Cow::Borrowed(value);
215        }
216        let mut out = Vec::with_capacity(value.len() + 4);
217        let mut last = 0;
218        let mut pos = 0;
219        while pos < bytes.len() {
220            let remaining = &bytes[pos..];
221            let hit_ecr = memchr::memchr3(elem, comp, release, remaining);
222            let hit_t = memchr::memchr(term, remaining);
223            let hit = match (hit_ecr, hit_t) {
224                (None, None) => break,
225                (Some(a), None) => a,
226                (None, Some(b)) => b,
227                (Some(a), Some(b)) => a.min(b),
228            };
229            let abs = pos + hit;
230            out.extend_from_slice(&bytes[last..abs]);
231            out.push(release);
232            out.push(bytes[abs]);
233            last = abs + 1;
234            pos = abs + 1;
235        }
236        out.extend_from_slice(&bytes[last..]);
237        // SAFETY:
238        //   1. `value` is a valid `&str`, so `bytes` is valid UTF-8 to start.
239        //   2. `self.ssa.release_char` is a single-byte ASCII value (0x21–0x7E),
240        //      enforced at construction time by `ServiceStringAdvice::is_valid()`
241        //      (called in `Writer::with_una`; the default SSA hardcodes `?` = 0x3F).
242        //      Inserting a single ASCII byte cannot split or corrupt a multi-byte
243        //      UTF-8 sequence, because ASCII bytes always have the high bit clear
244        //      while continuation bytes of multi-byte sequences always have the high
245        //      bit set (0x80–0xBF).
246        //   3. All other bytes are copied verbatim from the valid UTF-8 source.
247        Cow::Owned(
248            String::from_utf8(out).expect(
249                "escape_value: output is not valid UTF-8; this is a bug in the escape logic",
250            ),
251        )
252    }
253    /// Write only the segment tag bytes — no element separator or terminator.
254    ///
255    /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
256    #[inline]
257    pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
258        self.inner.write_all(tag.as_bytes())?;
259        Ok(())
260    }
261
262    /// Write one element separator byte.
263    #[inline]
264    pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
265        self.inner.write_all(&[self.ssa.element_sep])?;
266        Ok(())
267    }
268
269    /// Write one component separator byte.
270    #[inline]
271    pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
272        self.inner.write_all(&[self.ssa.component_sep])?;
273        Ok(())
274    }
275
276    /// Write the segment terminator and increment the internal segment counter.
277    #[inline]
278    pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
279        self.inner.write_all(&[self.ssa.segment_term])?;
280        self.segment_count += 1;
281        Ok(())
282    }
283
284    /// Write a value, escaping any delimiter characters.
285    pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
286        let (elem, comp, release, term) = (
287            self.ssa.element_sep,
288            self.ssa.component_sep,
289            self.ssa.release_char,
290            self.ssa.segment_term,
291        );
292        let bytes = value.as_bytes();
293        let mut last = 0;
294        let mut pos = 0;
295        while pos < bytes.len() {
296            // Use memchr3 for three delimiters + memchr for the fourth to avoid
297            // a manual byte-by-byte scan.
298            let remaining = &bytes[pos..];
299            let hit_ecr = memchr::memchr3(elem, comp, release, remaining);
300            let hit_t = memchr::memchr(term, remaining);
301            let hit = match (hit_ecr, hit_t) {
302                (None, None) => break,
303                (Some(a), None) => a,
304                (None, Some(b)) => b,
305                (Some(a), Some(b)) => a.min(b),
306            };
307            let abs = pos + hit;
308            if abs > last {
309                self.inner.write_all(&bytes[last..abs])?;
310            }
311            self.inner.write_all(&[release, bytes[abs]])?;
312            last = abs + 1;
313            pos = abs + 1;
314        }
315        self.inner.write_all(&bytes[last..])?;
316        Ok(())
317    }
318
319    // ── Interchange envelope helpers ──────────────────────────────────────────
320
321    /// Write a `UNB` interchange header segment.
322    ///
323    /// Generates:
324    /// ```text
325    /// UNB+<syntax_id>+<sender>+<recipient>+<datetime>+<control_ref>'
326    /// ```
327    ///
328    /// The caller is responsible for:
329    /// - Formatting `syntax_id` as a composite (e.g. `"UNOA:1"` for UN/EDIFACT syntax
330    ///   version 1 of set A).
331    /// - Formatting `datetime` as a composite date-time (e.g. `"200101:0900"`).
332    ///
333    /// Track the `control_ref` — it must be repeated in the matching
334    /// [`end_interchange`](Self::end_interchange) call.
335    ///
336    /// # Errors
337    ///
338    /// Returns [`EdifactError`] if writing fails.
339    pub fn begin_interchange(
340        &mut self,
341        syntax_id: &str,
342        sender: &str,
343        recipient: &str,
344        datetime: &str,
345        control_ref: &str,
346    ) -> Result<(), EdifactError> {
347        self.write_raw(
348            "UNB",
349            &[syntax_id, sender, recipient, datetime, control_ref],
350        )
351    }
352
353    /// Write a `UNH` message header and return a [`MessageWriter`] guard.
354    ///
355    /// The guard tracks the per-message segment count automatically.  Call
356    /// [`MessageWriter::finish`] when all message segments have been written — this
357    /// writes the matching `UNT` segment with the correct count.  If `finish` is not
358    /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
359    /// are silently discarded on drop; prefer explicit `finish`).
360    ///
361    /// Generates:
362    /// ```text
363    /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
364    /// ```
365    ///
366    /// # Errors
367    ///
368    /// Returns [`EdifactError`] if writing the `UNH` segment fails.
369    pub fn begin_message<'w>(
370        &'w mut self,
371        message_ref: &str,
372        message_type: &str,
373        version: &str,
374        release: &str,
375        controlling_agency: &str,
376    ) -> Result<MessageWriter<'w, W>, EdifactError> {
377        let msg_id = format!("{message_type}:{version}:{release}:{controlling_agency}");
378        self.write_raw("UNH", &[message_ref, &msg_id])?;
379        // Capture `segment_count` after writing UNH so `MessageWriter` knows
380        // the absolute count that includes UNH.
381        let unh_count = self.segment_count;
382        Ok(MessageWriter {
383            writer: self,
384            message_ref: message_ref.to_owned(),
385            unh_count,
386            finished: false,
387        })
388    }
389
390    /// Write a `UNZ` interchange trailer segment.
391    ///
392    /// `message_count` is the number of `UNH`/`UNT` message pairs in the
393    /// interchange.  `control_ref` must match the value passed to
394    /// [`begin_interchange`](Self::begin_interchange).
395    ///
396    /// If you used [`begin_message`](Self::begin_message) for every message in the
397    /// interchange, `message_count` equals the number of times you called that
398    /// method.
399    ///
400    /// # Errors
401    ///
402    /// Returns [`EdifactError`] if writing fails.
403    pub fn end_interchange(
404        &mut self,
405        message_count: u32,
406        control_ref: &str,
407    ) -> Result<(), EdifactError> {
408        let msg_count_str = message_count.to_string();
409        self.write_raw("UNZ", &[&msg_count_str, control_ref])
410    }
411}
412
413/// RAII guard for a single EDIFACT message within an interchange.
414///
415/// Obtained from [`Writer::begin_message`].  Writes `UNH` on creation and
416/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
417/// is called or the guard is dropped.
418///
419/// Always prefer calling [`finish`](Self::finish) explicitly so that write
420/// errors can be propagated.  The `Drop` impl writes `UNT` as a best-effort
421/// fallback but silently discards I/O errors.
422///
423/// # Example
424///
425/// ```rust,no_run
426/// # use edifact_rs::{Writer, Segment};
427/// # fn example() -> Result<(), edifact_rs::EdifactError> {
428/// let mut writer = Writer::new(Vec::new());
429/// writer.begin_interchange("UNOA:1", "SENDER", "RECEIVER", "200101:0900", "1")?;
430/// {
431///     let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
432///     msg.write_raw("BGM", &["220", "PO001", "9"])?;
433///     msg.finish()?;
434/// }
435/// writer.end_interchange(1, "1")?;
436/// # Ok(())
437/// # }
438/// ```
439pub struct MessageWriter<'w, W: Write> {
440    writer: &'w mut Writer<W>,
441    message_ref: String,
442    /// Absolute segment count immediately after `UNH` was written.
443    unh_count: u64,
444    /// Set to `true` once `finish()` has been called to prevent a double-write
445    /// from the `Drop` impl.
446    finished: bool,
447}
448
449impl<'w, W: Write> MessageWriter<'w, W> {
450    /// Write a segment within this message.
451    ///
452    /// Delegates to [`Writer::write_raw`].
453    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
454        self.writer.write_raw(tag, elements)
455    }
456
457    /// Write a fully-typed segment within this message.
458    ///
459    /// Delegates to [`Writer::write_segment`].
460    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
461        self.writer.write_segment(seg)
462    }
463
464    /// Compute the per-message segment count and write `UNT`, consuming the guard.
465    ///
466    /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
467    /// and `UNT` itself — matching the EDIFACT standard.
468    ///
469    /// # Errors
470    ///
471    /// Returns [`EdifactError`] if writing the `UNT` segment fails.
472    pub fn finish(mut self) -> Result<(), EdifactError> {
473        self.write_unt()?;
474        self.finished = true;
475        Ok(())
476    }
477
478    fn write_unt(&mut self) -> Result<(), EdifactError> {
479        // Segments since UNH: writer.segment_count - unh_count (content only).
480        // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
481        let count = self.writer.segment_count - self.unh_count + 2;
482        let count_str = count.to_string();
483        self.writer
484            .write_raw("UNT", &[&count_str, &self.message_ref])
485    }
486}
487
488impl<'w, W: Write> Drop for MessageWriter<'w, W> {
489    fn drop(&mut self) {
490        if !self.finished {
491            // Best-effort: write UNT; errors cannot be propagated from drop.
492            let _ = self.write_unt();
493        }
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500    use crate::model::Element;
501
502    #[test]
503    fn write_and_parse_simple_segment() {
504        let segs: Vec<Segment<'static>> = vec![Segment::new(
505            "BGM",
506            vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
507        )];
508        let bytes = crate::segments_to_bytes(&segs).unwrap();
509        let s = std::str::from_utf8(&bytes).unwrap();
510        assert!(s.starts_with("BGM+220+ORDER123'"));
511    }
512
513    #[test]
514    fn release_char_escaped() {
515        let segs: Vec<Segment<'static>> = vec![Segment::new(
516            "FTX",
517            vec![Element::of(&["value+with+delimiters"])],
518        )];
519        let bytes = crate::segments_to_bytes(&segs).unwrap();
520        let s = std::str::from_utf8(&bytes).unwrap();
521        // The `+` in the value must be escaped as `?+`
522        assert!(s.contains("?+"), "escape missing: {s}");
523    }
524
525    #[test]
526    fn round_trip_preserves_values() {
527        let segs: Vec<Segment<'static>> = vec![
528            Segment::new(
529                "UNB",
530                vec![
531                    Element::of(&["UNOA", "1"]),
532                    Element::of(&["SENDER"]),
533                    Element::of(&["RECEIVER"]),
534                ],
535            ),
536            Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
537        ];
538        let bytes = crate::segments_to_bytes(&segs).unwrap();
539        let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
540            .expect("round-trip parse failed");
541        assert_eq!(rt[0].tag, "UNB");
542        assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
543        assert_eq!(rt[1].tag, "UNZ");
544    }
545
546    /// Verify that `Writer::with_una` uses the configured delimiters throughout,
547    /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
548    /// component separators even with a non-default UNA.
549    #[test]
550    fn with_una_non_default_delimiters() {
551        use crate::tokenizer::ServiceStringAdvice;
552
553        // Custom UNA: comp_sep=|  elem_sep=!  esc=?  dec_mark=,  rep_sep=*  seg_term=~
554        let ssa = ServiceStringAdvice {
555            component_sep: b'|',
556            element_sep: b'!',
557            release_char: b'?',
558            decimal_mark: b',',
559            repetition_sep: b'*',
560            segment_term: b'~',
561        };
562
563        let buf = Vec::new();
564        let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
565
566        // write_segment_parts: pre-split; no hard-coded `:` in element strings
567        writer
568            .write_segment_parts(
569                "BGM",
570                &[
571                    vec!["220".to_owned(), "SUB1".to_owned()],
572                    vec!["PO1".to_owned()],
573                ],
574            )
575            .expect("write failed");
576
577        let out = writer.finish().expect("finish failed");
578        let s = std::str::from_utf8(&out).unwrap();
579
580        // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
581        // The writer also emits a UNA header when with_una is used.
582        assert!(s.contains("BGM"), "BGM segment missing: {s}");
583        // Slice after UNA so assertions target segment output, not UNA header bytes.
584        let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
585        assert!(
586            after_una.contains('!'),
587            "missing element sep in segment: {after_una}"
588        );
589        assert!(
590            after_una.contains('|'),
591            "missing component sep in segment: {after_una}"
592        );
593        assert!(
594            after_una.ends_with('~'),
595            "missing segment term in segment: {after_una}"
596        );
597        // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
598        assert!(s.contains(','), "missing decimal mark in UNA: {s}");
599        assert!(!s.contains('+'), "default element sep leaked: {s}");
600        assert!(!s.contains(':'), "default component sep leaked: {s}");
601        // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
602        assert!(
603            !after_una.contains('\''),
604            "default segment term leaked after UNA: {after_una}"
605        );
606    }
607}