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/// One data element of a segment being written: simple or composite.
8///
9/// The everyday EDIFACT segment mixes both shapes — `NAD+MS+id::agency`,
10/// `DTM+137:20260101:102` — and this enum lets a single call express that
11/// without pre-joining components into a string (which loses the distinction
12/// between a separator and a literal `:` in a value).
13///
14/// `From` impls cover the common literals, so `"MS".into()` and
15/// `["a", "", "b"].into()` both work; the [`elements!`][crate::elements] macro
16/// wraps that up entirely.
17///
18/// # Example
19///
20/// ```rust
21/// use edifact_rs::{DataElement, Writer};
22///
23/// let mut w = Writer::new(Vec::new());
24/// w.write_elements(
25///     "NAD",
26///     &[
27///         DataElement::Simple("MS"),
28///         DataElement::Composite(&["9900112233445", "", "293"]),
29///     ],
30/// )?;
31/// assert_eq!(w.finish()?, b"NAD+MS+9900112233445::293'".to_vec());
32/// # Ok::<(), edifact_rs::EdifactError>(())
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum DataElement<'a> {
36    /// A simple data element — one value, no component separators.
37    Simple(&'a str),
38    /// A composite data element — components written in order, separated by the
39    /// active component separator.  A separator byte *inside* a component value
40    /// is escaped rather than promoted to a boundary.
41    Composite(&'a [&'a str]),
42}
43
44impl<'a> DataElement<'a> {
45    /// The components of this element, as a slice.
46    #[inline]
47    #[must_use]
48    pub fn components(&self) -> &[&'a str] {
49        match self {
50            Self::Simple(value) => std::slice::from_ref(value),
51            Self::Composite(components) => components,
52        }
53    }
54}
55
56impl<'a> From<&'a str> for DataElement<'a> {
57    #[inline]
58    fn from(value: &'a str) -> Self {
59        Self::Simple(value)
60    }
61}
62
63impl<'a> From<&'a [&'a str]> for DataElement<'a> {
64    #[inline]
65    fn from(components: &'a [&'a str]) -> Self {
66        Self::Composite(components)
67    }
68}
69
70impl<'a, const N: usize> From<&'a [&'a str; N]> for DataElement<'a> {
71    #[inline]
72    fn from(components: &'a [&'a str; N]) -> Self {
73        Self::Composite(components)
74    }
75}
76
77/// Borrow a value as a [`DataElement`], choosing simple or composite by type.
78///
79/// A single string borrows as [`DataElement::Simple`]; an array, slice, or `Vec`
80/// of strings borrows as [`DataElement::Composite`]. This is what lets the
81/// [`elements!`][crate::elements] macro accept both shapes from arbitrary
82/// expressions rather than only from literals.
83///
84/// # Example
85///
86/// ```rust
87/// use edifact_rs::{AsDataElement, DataElement};
88///
89/// let qualifier = String::from("MS");
90/// let party = ["9900112233445", "", "293"];
91///
92/// assert_eq!(qualifier.as_data_element(), DataElement::Simple("MS"));
93/// assert_eq!(
94///     party.as_data_element(),
95///     DataElement::Composite(&["9900112233445", "", "293"]),
96/// );
97/// ```
98pub trait AsDataElement {
99    /// Borrow `self` as a [`DataElement`].
100    fn as_data_element(&self) -> DataElement<'_>;
101}
102
103impl AsDataElement for str {
104    #[inline]
105    fn as_data_element(&self) -> DataElement<'_> {
106        DataElement::Simple(self)
107    }
108}
109
110impl AsDataElement for &str {
111    #[inline]
112    fn as_data_element(&self) -> DataElement<'_> {
113        DataElement::Simple(self)
114    }
115}
116
117impl AsDataElement for String {
118    #[inline]
119    fn as_data_element(&self) -> DataElement<'_> {
120        DataElement::Simple(self.as_str())
121    }
122}
123
124impl AsDataElement for Cow<'_, str> {
125    #[inline]
126    fn as_data_element(&self) -> DataElement<'_> {
127        DataElement::Simple(self.as_ref())
128    }
129}
130
131impl<const N: usize> AsDataElement for [&str; N] {
132    #[inline]
133    fn as_data_element(&self) -> DataElement<'_> {
134        DataElement::Composite(self)
135    }
136}
137
138impl AsDataElement for [&str] {
139    #[inline]
140    fn as_data_element(&self) -> DataElement<'_> {
141        DataElement::Composite(self)
142    }
143}
144
145impl AsDataElement for Vec<&str> {
146    #[inline]
147    fn as_data_element(&self) -> DataElement<'_> {
148        DataElement::Composite(self)
149    }
150}
151
152impl AsDataElement for DataElement<'_> {
153    #[inline]
154    fn as_data_element(&self) -> DataElement<'_> {
155        *self
156    }
157}
158
159/// Build a `&[`[`DataElement`]`]` from a mix of simple values and component lists.
160///
161/// Each entry is an arbitrary expression borrowed through
162/// [`AsDataElement`]: a string becomes a simple data element, an array or slice
163/// of strings becomes a composite. This is the shorthand for the mixed-segment
164/// shape that dominates real EDIFACT:
165///
166/// ```rust
167/// use edifact_rs::{Writer, elements};
168///
169/// // Runtime values, not just literals — the everyday builder shape.
170/// let qualifier = String::from("MS");
171/// let gln = "9900112233445";
172///
173/// let mut w = Writer::new(Vec::new());
174/// w.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])?;
175/// w.write_elements("DTM", elements![["137", "20260101", "102"]])?;
176/// assert_eq!(
177///     w.finish()?,
178///     b"NAD+MS+9900112233445::293'DTM+137:20260101:102'".to_vec(),
179/// );
180/// # Ok::<(), edifact_rs::EdifactError>(())
181/// ```
182///
183/// Composite components must be string *slices*: a `[String; N]` cannot borrow
184/// as `&[&str]` without allocating, so write `[id.as_str(), "", agency]`.
185///
186/// The expansion borrows temporaries, so the result must be consumed within the
187/// same statement — passing it directly as an argument, as above, always is.
188#[macro_export]
189macro_rules! elements {
190    () => {
191        &[] as &[$crate::DataElement<'_>]
192    };
193    ($($element:expr),+ $(,)?) => {
194        &[$($crate::AsDataElement::as_data_element(&$element)),+][..]
195    };
196}
197
198/// Streaming EDIFACT writer.
199///
200/// Wraps any [`Write`] implementation and serializes segments one at a time.
201/// Call [`Writer::finish`] to flush and get the underlying writer back.
202pub struct Writer<W: Write> {
203    inner: W,
204    ssa: ServiceStringAdvice,
205    /// Running count of segments written.  `u64` to prevent silent overflow on
206    /// pathological inputs (a `u32` would wrap after ~4 billion segments).
207    segment_count: u64,
208    /// `segment_count` as of the most recent `UNH`, used by [`Writer::finish_unt`]
209    /// to derive a per-message DE 0074 rather than a writer-lifetime total.
210    message_start_count: u64,
211}
212
213/// Return the offset of the first byte in `hay` that must be release-escaped.
214///
215/// The escape set is the four splitting delimiters plus the repetition separator
216/// when the active UNA declares one.  A space at UNA position 7 is the
217/// conventional "not used" sentinel and is never escaped.
218#[inline]
219fn find_escape(ssa: &ServiceStringAdvice, hay: &[u8]) -> Option<usize> {
220    let first = memchr::memchr3(ssa.element_sep, ssa.component_sep, ssa.release_char, hay);
221    let second = if ssa.repetition_sep == b' ' {
222        memchr::memchr(ssa.segment_term, hay)
223    } else {
224        memchr::memchr2(ssa.segment_term, ssa.repetition_sep, hay)
225    };
226    match (first, second) {
227        (None, None) => None,
228        (Some(a), None) => Some(a),
229        (None, Some(b)) => Some(b),
230        (Some(a), Some(b)) => Some(a.min(b)),
231    }
232}
233
234impl<W: Write> Writer<W> {
235    /// Create a new writer with default EDIFACT delimiters.
236    pub fn new(inner: W) -> Self {
237        Self {
238            inner,
239            ssa: ServiceStringAdvice::default(),
240            segment_count: 0,
241            message_start_count: 0,
242        }
243    }
244
245    /// Create a writer with custom delimiters and write a UNA segment first.
246    pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
247        // All five active service characters must be mutually distinct, non-whitespace,
248        // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
249        if !ssa.is_valid() {
250            return Err(EdifactError::InvalidUna);
251        }
252        // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
253        let una = [
254            b'U',
255            b'N',
256            b'A',
257            ssa.component_sep,
258            ssa.element_sep,
259            ssa.decimal_mark,
260            ssa.release_char,
261            ssa.repetition_sep,
262            ssa.segment_term,
263        ];
264        inner.write_all(&una)?;
265        Ok(Self {
266            inner,
267            ssa,
268            segment_count: 0,
269            message_start_count: 0,
270        })
271    }
272
273    /// Write a single segment.
274    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
275        // Tag
276        self.inner.write_all(seg.tag.as_bytes())?;
277
278        for element in &seg.elements {
279            // Element separator
280            self.inner.write_all(&[self.ssa.element_sep])?;
281            let mut first_component = true;
282            for (component, _) in &element.components {
283                if !first_component {
284                    self.inner.write_all(&[self.ssa.component_sep])?;
285                }
286                first_component = false;
287                self.write_escaped(component)?;
288            }
289        }
290
291        // Segment terminator
292        self.inner.write_all(&[self.ssa.segment_term])?;
293        self.segment_count += 1;
294        Ok(())
295    }
296
297    /// Write a raw segment from tag + element string slices.
298    ///
299    /// Each element string is split on the **active component-separator byte** from the
300    /// configured [`ServiceStringAdvice`][crate::ServiceStringAdvice] to identify component
301    /// boundaries.  The default component separator is `:` (0x3A), but this can differ when a
302    /// non-default `UNA` string was used to construct the writer.
303    ///
304    /// # Delimiter dependency
305    ///
306    /// Callers that embed the literal `:` character in element strings rely on `:` being
307    /// the component separator.  When the writer uses a non-default delimiter set, `:` will
308    /// **not** be treated as a component boundary and the segment will be written incorrectly.
309    ///
310    /// **UTF-8 safety**: EDIFACT syntax requires all delimiter bytes to be single-byte ASCII
311    /// characters (values 0x00–0x7F).  Non-ASCII delimiter bytes would bisect multi-byte UTF-8
312    /// sequences in data values and produce malformed output.  All fields of
313    /// [`ServiceStringAdvice`][crate::ServiceStringAdvice] must therefore hold ASCII byte values.
314    ///
315    /// To produce correct output regardless of the active delimiter, prefer
316    /// [`Self::write_elements`] — it takes component boundaries explicitly and
317    /// handles the mixed simple/composite shape that most real segments have.
318    /// [`Self::write_segment_parts`] is the equivalent for owned data.
319    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
320        self.inner.write_all(tag.as_bytes())?;
321        let comp_sep = self.ssa.component_sep;
322        for el in elements {
323            self.inner.write_all(&[self.ssa.element_sep])?;
324            // Byte-level split: EDIFACT delimiters are always single bytes.
325            let mut parts = el.as_bytes().split(|&b| b == comp_sep);
326            if let Some(first) = parts.next() {
327                // INVARIANT: input is valid UTF-8 and we split on a single-byte ASCII
328                // delimiter, so each part remains a valid UTF-8 slice.
329                self.write_escaped(
330                    std::str::from_utf8(first).map_err(|_| EdifactError::InvalidUtf8)?,
331                )?;
332            }
333            for part in parts {
334                self.inner.write_all(&[comp_sep])?;
335                self.write_escaped(
336                    std::str::from_utf8(part).map_err(|_| EdifactError::InvalidUtf8)?,
337                )?;
338            }
339        }
340        self.inner.write_all(&[self.ssa.segment_term])?;
341        if tag == "UNH" {
342            self.message_start_count = self.segment_count;
343        }
344        self.segment_count += 1;
345        Ok(())
346    }
347
348    /// Write a segment from a tag and pre-split element/component data.
349    ///
350    /// `elements` is a slice of elements; each element is a sequence of component strings.
351    /// This avoids the lifetime constraints of [`Self::write_segment`] when building
352    /// segments from runtime-owned data (e.g. inside [`crate::WriterEmitter`]).
353    pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
354    where
355        E: AsRef<[String]>,
356    {
357        self.inner.write_all(tag.as_bytes())?;
358        for element in elements {
359            self.inner.write_all(&[self.ssa.element_sep])?;
360            let mut first = true;
361            for comp in element.as_ref() {
362                if !first {
363                    self.inner.write_all(&[self.ssa.component_sep])?;
364                }
365                first = false;
366                self.write_escaped(comp.as_str())?;
367            }
368        }
369        self.inner.write_all(&[self.ssa.segment_term])?;
370        self.segment_count += 1;
371        Ok(())
372    }
373
374    /// Write a segment from a tag and borrowed element/component slices.
375    ///
376    /// Unlike [`Self::write_raw`], component boundaries are given explicitly
377    /// rather than inferred by splitting on the active component separator, so
378    /// values containing a literal separator byte are escaped instead of being
379    /// silently reinterpreted as a composite boundary.  Unlike
380    /// [`Self::write_segment_parts`], no `String` allocation is required.
381    ///
382    /// # Example
383    ///
384    /// ```
385    /// use edifact_rs::Writer;
386    /// let mut w = Writer::new(Vec::new());
387    /// // The `:` inside the sender id stays part of the value.
388    /// w.write_composites("NAD", &[&["MS"][..], &["ACME:INC"][..]])?;
389    /// assert_eq!(w.finish()?, b"NAD+MS+ACME?:INC'".to_vec());
390    /// # Ok::<(), edifact_rs::EdifactError>(())
391    /// ```
392    ///
393    /// # Errors
394    ///
395    /// Returns [`EdifactError`] if the underlying writer fails.
396    pub fn write_composites(
397        &mut self,
398        tag: &str,
399        elements: &[&[&str]],
400    ) -> Result<(), EdifactError> {
401        self.inner.write_all(tag.as_bytes())?;
402        for element in elements {
403            self.inner.write_all(&[self.ssa.element_sep])?;
404            for (i, comp) in element.iter().enumerate() {
405                if i > 0 {
406                    self.inner.write_all(&[self.ssa.component_sep])?;
407                }
408                self.write_escaped(comp)?;
409            }
410        }
411        self.inner.write_all(&[self.ssa.segment_term])?;
412        if tag == "UNH" {
413            self.message_start_count = self.segment_count;
414        }
415        self.segment_count += 1;
416        Ok(())
417    }
418
419    /// Write a segment whose data elements mix simple and composite shapes.
420    ///
421    /// This is the general form of segment emission and the one that matches
422    /// how EDIFACT segments are actually specified: `NAD` takes a simple
423    /// qualifier followed by a composite party identification, `DTM` takes a
424    /// single composite.  [`write_raw`][Self::write_raw] (all-simple, with
425    /// separators inferred by splitting) and
426    /// [`write_composites`][Self::write_composites] (all-composite) are the two
427    /// special cases.
428    ///
429    /// Component boundaries are explicit, so a value containing the active
430    /// component separator is escaped rather than silently promoted to a
431    /// boundary.  Nothing is allocated.
432    ///
433    /// # Example
434    ///
435    /// ```rust
436    /// use edifact_rs::{DataElement, Writer, elements};
437    ///
438    /// let mut w = Writer::new(Vec::new());
439    /// // Explicit form …
440    /// w.write_elements(
441    ///     "NAD",
442    ///     &[DataElement::Simple("MS"), DataElement::Composite(&["ACME:INC", "", "9"])],
443    /// )?;
444    /// // … or the `elements!` shorthand.
445    /// w.write_elements("DTM", elements![["137", "20260101", "102"]])?;
446    /// assert_eq!(
447    ///     w.finish()?,
448    ///     b"NAD+MS+ACME?:INC::9'DTM+137:20260101:102'".to_vec(),
449    /// );
450    /// # Ok::<(), edifact_rs::EdifactError>(())
451    /// ```
452    ///
453    /// # Errors
454    ///
455    /// Returns [`EdifactError`] if the underlying writer fails.
456    pub fn write_elements(
457        &mut self,
458        tag: &str,
459        elements: &[DataElement<'_>],
460    ) -> Result<(), EdifactError> {
461        self.inner.write_all(tag.as_bytes())?;
462        for element in elements {
463            self.inner.write_all(&[self.ssa.element_sep])?;
464            for (i, comp) in element.components().iter().enumerate() {
465                if i > 0 {
466                    self.inner.write_all(&[self.ssa.component_sep])?;
467                }
468                self.write_escaped(comp)?;
469            }
470        }
471        self.inner.write_all(&[self.ssa.segment_term])?;
472        if tag == "UNH" {
473            self.message_start_count = self.segment_count;
474        }
475        self.segment_count += 1;
476        Ok(())
477    }
478
479    /// Flush and return the underlying writer.
480    pub fn finish(mut self) -> Result<W, EdifactError> {
481        self.inner.flush()?;
482        Ok(self.inner)
483    }
484
485    /// Write the `UNT` segment and return the inner writer.
486    ///
487    /// The count written into `UNT` DE 0074 covers the current message only:
488    /// `UNH`, every segment written since it, and `UNT` itself.  Segments written
489    /// before the message's `UNH` — an interchange-level `UNB`, or a preceding
490    /// message — are excluded, as EDIFACT requires.
491    ///
492    /// If no `UNH` has been written, the count falls back to every segment
493    /// written so far plus one.
494    ///
495    /// # Errors
496    ///
497    /// Returns an error if writing fails.  Do **not** call [`write_raw`][Self::write_raw] or
498    /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
499    pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
500        // DE 0074 counts UNH + content + UNT.  `message_start_count` is the
501        // absolute segment count immediately after UNH, so content is
502        // `segment_count - message_start_count` and the total adds UNH and UNT.
503        let count = self.segment_count - self.message_start_count + 1;
504        let count_str = count.to_string();
505        self.write_composites("UNT", &[&[count_str.as_str()], &[message_ref]])?;
506        self.finish()
507    }
508
509    /// Returns the total number of segments written so far.
510    pub fn segment_count(&self) -> u64 {
511        self.segment_count
512    }
513
514    /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
515    pub fn service_string_advice(&self) -> ServiceStringAdvice {
516        self.ssa
517    }
518
519    /// Escape a value string for inclusion in an EDIFACT segment.
520    ///
521    /// Any character in `value` that matches the active element separator,
522    /// component separator, release character, or segment terminator is escaped
523    /// by prefixing it with the release character (default `?`).
524    ///
525    /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
526    /// avoiding an allocation on the fast path.
527    ///
528    /// # Example
529    ///
530    /// ```rust,ignore
531    /// let writer = Writer::new(std::io::sink());
532    /// // '+' must be escaped since it is the default element separator.
533    /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
534    /// ```
535    pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
536        let release = self.ssa.release_char;
537        let bytes = value.as_bytes();
538        if find_escape(&self.ssa, bytes).is_none() {
539            return Cow::Borrowed(value);
540        }
541        let mut out = Vec::with_capacity(value.len() + 4);
542        let mut last = 0;
543        let mut pos = 0;
544        while pos < bytes.len() {
545            let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
546                break;
547            };
548            let abs = pos + hit;
549            out.extend_from_slice(&bytes[last..abs]);
550            out.push(release);
551            out.push(bytes[abs]);
552            last = abs + 1;
553            pos = abs + 1;
554        }
555        out.extend_from_slice(&bytes[last..]);
556        // SAFETY:
557        //   1. `value` is a valid `&str`, so `bytes` is valid UTF-8 to start.
558        //   2. `self.ssa.release_char` is a single-byte ASCII value (0x21–0x7E),
559        //      enforced at construction time by `ServiceStringAdvice::is_valid()`
560        //      (called in `Writer::with_una`; the default SSA hardcodes `?` = 0x3F).
561        //      Inserting a single ASCII byte cannot split or corrupt a multi-byte
562        //      UTF-8 sequence, because ASCII bytes always have the high bit clear
563        //      while continuation bytes of multi-byte sequences always have the high
564        //      bit set (0x80–0xBF).
565        //   3. All other bytes are copied verbatim from the valid UTF-8 source.
566        Cow::Owned(
567            String::from_utf8(out).expect(
568                "escape_value: output is not valid UTF-8; this is a bug in the escape logic",
569            ),
570        )
571    }
572    /// Write only the segment tag bytes — no element separator or terminator.
573    ///
574    /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
575    #[inline]
576    pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
577        self.inner.write_all(tag.as_bytes())?;
578        Ok(())
579    }
580
581    /// Write one element separator byte.
582    #[inline]
583    pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
584        self.inner.write_all(&[self.ssa.element_sep])?;
585        Ok(())
586    }
587
588    /// Write one component separator byte.
589    #[inline]
590    pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
591        self.inner.write_all(&[self.ssa.component_sep])?;
592        Ok(())
593    }
594
595    /// Write the segment terminator and increment the internal segment counter.
596    #[inline]
597    pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
598        self.inner.write_all(&[self.ssa.segment_term])?;
599        self.segment_count += 1;
600        Ok(())
601    }
602
603    /// Write a value, escaping any delimiter characters.
604    pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
605        let release = self.ssa.release_char;
606        let bytes = value.as_bytes();
607        let mut last = 0;
608        let mut pos = 0;
609        while pos < bytes.len() {
610            let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
611                break;
612            };
613            let abs = pos + hit;
614            if abs > last {
615                self.inner.write_all(&bytes[last..abs])?;
616            }
617            self.inner.write_all(&[release, bytes[abs]])?;
618            last = abs + 1;
619            pos = abs + 1;
620        }
621        self.inner.write_all(&bytes[last..])?;
622        Ok(())
623    }
624
625    // ── Interchange envelope helpers ──────────────────────────────────────────
626
627    /// Write a `UNB` interchange header segment.
628    ///
629    /// Generates:
630    /// ```text
631    /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
632    /// ```
633    ///
634    /// Composite components (S001 syntax identifier/version, S004 date/time) are
635    /// passed separately rather than pre-joined with `:`, so they are written
636    /// with the writer's *active* component separator and so a literal separator
637    /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
638    /// silently promoted to a component boundary.
639    ///
640    /// Track the `control_ref` — it must be repeated in the matching
641    /// [`end_interchange`](Self::end_interchange) call.
642    ///
643    /// # Example
644    ///
645    /// ```
646    /// use edifact_rs::Writer;
647    /// let mut w = Writer::new(Vec::new());
648    /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
649    /// assert_eq!(
650    ///     w.finish()?,
651    ///     b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
652    /// );
653    /// # Ok::<(), edifact_rs::EdifactError>(())
654    /// ```
655    ///
656    /// # Errors
657    ///
658    /// Returns [`EdifactError`] if writing fails.
659    #[allow(clippy::too_many_arguments)]
660    pub fn begin_interchange(
661        &mut self,
662        syntax_id: &str,
663        syntax_version: &str,
664        sender: &str,
665        recipient: &str,
666        date: &str,
667        time: &str,
668        control_ref: &str,
669    ) -> Result<(), EdifactError> {
670        self.write_composites(
671            "UNB",
672            &[
673                &[syntax_id, syntax_version],
674                &[sender],
675                &[recipient],
676                &[date, time],
677                &[control_ref],
678            ],
679        )
680    }
681
682    /// Write a `UNH` message header and return a [`MessageWriter`] guard.
683    ///
684    /// The guard tracks the per-message segment count automatically.  Call
685    /// [`MessageWriter::finish`] when all message segments have been written — this
686    /// writes the matching `UNT` segment with the correct count.  If `finish` is not
687    /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
688    /// are silently discarded on drop; prefer explicit `finish`).
689    ///
690    /// Generates:
691    /// ```text
692    /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
693    /// ```
694    ///
695    /// # Errors
696    ///
697    /// Returns [`EdifactError`] if writing the `UNH` segment fails.
698    pub fn begin_message<'w>(
699        &'w mut self,
700        message_ref: &str,
701        message_type: &str,
702        version: &str,
703        release: &str,
704        controlling_agency: &str,
705    ) -> Result<MessageWriter<'w, W>, EdifactError> {
706        // Build S009 as an explicit composite.  Formatting it with a literal `:`
707        // and handing it to `write_raw` produced a single collapsed component
708        // whenever the writer used a non-default component separator.
709        self.write_composites(
710            "UNH",
711            &[
712                &[message_ref],
713                &[message_type, version, release, controlling_agency],
714            ],
715        )?;
716        // Capture `segment_count` after writing UNH so `MessageWriter` knows
717        // the absolute count that includes UNH.
718        let unh_count = self.segment_count;
719        Ok(MessageWriter {
720            writer: self,
721            message_ref: message_ref.to_owned(),
722            unh_count,
723            finished: false,
724        })
725    }
726
727    /// Write a `UNZ` interchange trailer segment.
728    ///
729    /// `message_count` is the number of `UNH`/`UNT` message pairs in the
730    /// interchange.  `control_ref` must match the value passed to
731    /// [`begin_interchange`](Self::begin_interchange).
732    ///
733    /// If you used [`begin_message`](Self::begin_message) for every message in the
734    /// interchange, `message_count` equals the number of times you called that
735    /// method.
736    ///
737    /// # Errors
738    ///
739    /// Returns [`EdifactError`] if writing fails.
740    pub fn end_interchange(
741        &mut self,
742        message_count: u32,
743        control_ref: &str,
744    ) -> Result<(), EdifactError> {
745        let msg_count_str = message_count.to_string();
746        self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
747    }
748}
749
750/// RAII guard for a single EDIFACT message within an interchange.
751///
752/// Obtained from [`Writer::begin_message`].  Writes `UNH` on creation and
753/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
754/// is called or the guard is dropped.
755///
756/// Always prefer calling [`finish`](Self::finish) explicitly so that write
757/// errors can be propagated.  The `Drop` impl writes `UNT` as a best-effort
758/// fallback but silently discards I/O errors.
759///
760/// # Example
761///
762/// ```rust,no_run
763/// # use edifact_rs::{Writer, Segment};
764/// # fn example() -> Result<(), edifact_rs::EdifactError> {
765/// let mut writer = Writer::new(Vec::new());
766/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
767/// {
768///     let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
769///     msg.write_raw("BGM", &["220", "PO001", "9"])?;
770///     msg.finish()?;
771/// }
772/// writer.end_interchange(1, "1")?;
773/// # Ok(())
774/// # }
775/// ```
776pub struct MessageWriter<'w, W: Write> {
777    writer: &'w mut Writer<W>,
778    message_ref: String,
779    /// Absolute segment count immediately after `UNH` was written.
780    unh_count: u64,
781    /// Set to `true` once `finish()` has been called to prevent a double-write
782    /// from the `Drop` impl.
783    finished: bool,
784}
785
786impl<W: Write> MessageWriter<'_, W> {
787    /// Write a segment within this message.
788    ///
789    /// Delegates to [`Writer::write_raw`].
790    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
791        self.writer.write_raw(tag, elements)
792    }
793
794    /// Write a segment mixing simple and composite data elements within this message.
795    ///
796    /// Delegates to [`Writer::write_elements`] — the general form, and the one
797    /// to reach for when a segment is not uniformly simple or uniformly
798    /// composite.
799    ///
800    /// # Errors
801    ///
802    /// Returns [`EdifactError`] if the underlying writer fails.
803    pub fn write_elements(
804        &mut self,
805        tag: &str,
806        elements: &[DataElement<'_>],
807    ) -> Result<(), EdifactError> {
808        self.writer.write_elements(tag, elements)
809    }
810
811    /// Write a segment from borrowed element/component slices within this message.
812    ///
813    /// Delegates to [`Writer::write_composites`].
814    ///
815    /// # Errors
816    ///
817    /// Returns [`EdifactError`] if the underlying writer fails.
818    pub fn write_composites(
819        &mut self,
820        tag: &str,
821        elements: &[&[&str]],
822    ) -> Result<(), EdifactError> {
823        self.writer.write_composites(tag, elements)
824    }
825
826    /// Write a segment from pre-split, owned element/component data within this message.
827    ///
828    /// Delegates to [`Writer::write_segment_parts`].
829    ///
830    /// # Errors
831    ///
832    /// Returns [`EdifactError`] if the underlying writer fails.
833    pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
834    where
835        E: AsRef<[String]>,
836    {
837        self.writer.write_segment_parts(tag, elements)
838    }
839
840    /// Write a fully-typed segment within this message.
841    ///
842    /// Delegates to [`Writer::write_segment`].
843    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
844        self.writer.write_segment(seg)
845    }
846
847    /// Compute the per-message segment count and write `UNT`, consuming the guard.
848    ///
849    /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
850    /// and `UNT` itself — matching the EDIFACT standard.
851    ///
852    /// # Errors
853    ///
854    /// Returns [`EdifactError`] if writing the `UNT` segment fails.
855    pub fn finish(mut self) -> Result<(), EdifactError> {
856        self.write_unt()?;
857        self.finished = true;
858        Ok(())
859    }
860
861    fn write_unt(&mut self) -> Result<(), EdifactError> {
862        // Segments since UNH: writer.segment_count - unh_count (content only).
863        // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
864        let count = self.writer.segment_count - self.unh_count + 2;
865        let count_str = count.to_string();
866        self.writer.write_composites(
867            "UNT",
868            &[&[count_str.as_str()], &[self.message_ref.as_str()]],
869        )
870    }
871}
872
873impl<W: Write> Drop for MessageWriter<'_, W> {
874    fn drop(&mut self) {
875        if !self.finished {
876            // Best-effort: write UNT; errors cannot be propagated from drop.
877            let _ = self.write_unt();
878        }
879    }
880}
881
882#[cfg(test)]
883mod tests {
884    use super::*;
885    use crate::model::Element;
886
887    /// A non-default UNA whose delimiters share no byte with the defaults.
888    fn exotic_ssa() -> ServiceStringAdvice {
889        ServiceStringAdvice {
890            component_sep: b'|',
891            element_sep: b'!',
892            decimal_mark: b',',
893            release_char: b'#',
894            repetition_sep: b'*',
895            segment_term: b'~',
896        }
897    }
898
899    #[test]
900    fn unh_composite_uses_the_active_component_separator() {
901        // `begin_message` used to `format!` the S009 composite with a literal
902        // `:`, collapsing it into one component under a custom UNA.
903        let mut buf = Vec::new();
904        {
905            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
906            let msg = w
907                .begin_message("1", "ORDERS", "D", "96A", "UN")
908                .expect("UNH");
909            msg.finish().expect("UNT");
910        }
911        let out = String::from_utf8(buf).unwrap();
912        assert!(
913            out.contains("UNH!1!ORDERS|D|96A|UN~"),
914            "S009 must use `|`, got {out}"
915        );
916    }
917
918    #[test]
919    fn round_trips_through_a_custom_una() {
920        // The library must be able to re-read its own output verbatim.
921        let mut buf = Vec::new();
922        {
923            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
924            w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
925                .unwrap();
926            let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
927            msg.write_raw("BGM", &["220"]).unwrap();
928            msg.finish().unwrap();
929            w.end_interchange(1, "IC1").unwrap();
930        }
931        let segs: Vec<_> = crate::from_bytes(&buf)
932            .collect::<Result<Vec<_>, _>>()
933            .expect("own output must reparse");
934        let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
935        assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
936        assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
937        crate::validate_envelope(&segs).expect("own output must pass envelope validation");
938    }
939
940    #[test]
941    fn finish_unt_counts_only_the_current_message() {
942        // `finish_unt` used the writer-lifetime segment total, so a preceding
943        // UNB inflated DE 0074 and the interchange failed its own validation.
944        let mut buf = Vec::new();
945        {
946            let mut w = Writer::new(&mut buf);
947            w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
948                .unwrap();
949            w.write_composites("UNH", &[&["1"], &["ORDERS", "D", "96A", "UN"]])
950                .unwrap();
951            w.write_raw("BGM", &["220"]).unwrap();
952            w.finish_unt("1").unwrap();
953        }
954        let out = String::from_utf8(buf).unwrap();
955        // UNH + BGM + UNT == 3
956        assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
957    }
958
959    #[test]
960    fn repetition_separator_is_escaped_when_declared() {
961        let mut buf = Vec::new();
962        {
963            let mut w = Writer::with_una(
964                &mut buf,
965                ServiceStringAdvice {
966                    repetition_sep: b'*',
967                    ..ServiceStringAdvice::default()
968                },
969            )
970            .unwrap();
971            w.write_composites("FTX", &[&["a*b"]]).unwrap();
972        }
973        let out = String::from_utf8(buf).unwrap();
974        assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
975    }
976
977    #[test]
978    fn repetition_separator_sentinel_is_not_escaped() {
979        // Space at UNA position 7 means "not used" and must never be escaped.
980        let w = Writer::new(std::io::sink());
981        assert_eq!(w.escape_value("a b"), "a b");
982    }
983
984    #[test]
985    fn write_composites_escapes_a_literal_component_separator() {
986        let mut buf = Vec::new();
987        {
988            let mut w = Writer::new(&mut buf);
989            w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
990                .unwrap();
991        }
992        let segs: Vec<_> = crate::from_bytes(&buf)
993            .collect::<Result<Vec<_>, _>>()
994            .unwrap();
995        // The `:` stays inside the value instead of splitting the element.
996        assert_eq!(
997            segs[0].get_element(1).unwrap().get_component(0),
998            Some("ACME:INC")
999        );
1000    }
1001
1002    #[test]
1003    fn write_elements_mixes_simple_and_composite() {
1004        let mut buf = Vec::new();
1005        {
1006            let mut w = Writer::new(&mut buf);
1007            w.write_elements(
1008                "NAD",
1009                &[
1010                    DataElement::Simple("MS"),
1011                    DataElement::Composite(&["9900112233445", "", "293"]),
1012                ],
1013            )
1014            .unwrap();
1015        }
1016        assert_eq!(buf, b"NAD+MS+9900112233445::293'");
1017    }
1018
1019    #[test]
1020    fn elements_macro_matches_the_explicit_form() {
1021        let mut macro_buf = Vec::new();
1022        {
1023            let mut w = Writer::new(&mut macro_buf);
1024            w.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1025                .unwrap();
1026            w.write_elements("DTM", elements![["137", "20260101", "102"]])
1027                .unwrap();
1028        }
1029        let mut explicit_buf = Vec::new();
1030        {
1031            let mut w = Writer::new(&mut explicit_buf);
1032            w.write_elements(
1033                "NAD",
1034                &[
1035                    DataElement::Simple("MS"),
1036                    DataElement::Composite(&["ACME", "", "9"]),
1037                ],
1038            )
1039            .unwrap();
1040            w.write_elements(
1041                "DTM",
1042                &[DataElement::Composite(&["137", "20260101", "102"])],
1043            )
1044            .unwrap();
1045        }
1046        assert_eq!(macro_buf, explicit_buf);
1047        assert_eq!(macro_buf, b"NAD+MS+ACME::9'DTM+137:20260101:102'");
1048    }
1049
1050    #[test]
1051    fn elements_macro_accepts_arbitrary_expressions() {
1052        // Builders emit runtime values, not literals.  A `tt`-based macro only
1053        // matched single-token entries, so `qualifier.as_str()` failed to parse
1054        // — which is precisely the shape this macro exists for.
1055        let qualifier = String::from("MS");
1056        let gln = "9900112233445";
1057        let dtm: Vec<&str> = vec!["137", "20260101", "102"];
1058
1059        let mut buf = Vec::new();
1060        {
1061            let mut w = Writer::new(&mut buf);
1062            w.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])
1063                .unwrap();
1064            w.write_elements("DTM", elements![dtm]).unwrap();
1065            w.write_elements("FTX", elements![qualifier]).unwrap();
1066            w.write_elements("UNS", elements![]).unwrap();
1067        }
1068        assert_eq!(
1069            String::from_utf8(buf).unwrap(),
1070            "NAD+MS+9900112233445::293'DTM+137:20260101:102'FTX+MS'UNS'"
1071        );
1072    }
1073
1074    #[test]
1075    fn write_elements_escapes_a_literal_component_separator() {
1076        // The `:` stays inside the value instead of splitting the element —
1077        // the failure mode of pre-joining components into one string.
1078        let mut buf = Vec::new();
1079        {
1080            let mut w = Writer::new(&mut buf);
1081            w.write_elements(
1082                "NAD",
1083                &[DataElement::Simple("MS"), DataElement::Simple("ACME:INC")],
1084            )
1085            .unwrap();
1086        }
1087        let segs: Vec<_> = crate::from_bytes(&buf)
1088            .collect::<Result<Vec<_>, _>>()
1089            .unwrap();
1090        assert_eq!(
1091            segs[0].get_element(1).unwrap().get_component(0),
1092            Some("ACME:INC")
1093        );
1094    }
1095
1096    #[test]
1097    fn write_elements_uses_the_active_component_separator() {
1098        let mut buf = Vec::new();
1099        {
1100            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1101            w.write_elements("DTM", elements![["137", "20260101", "102"]])
1102                .unwrap();
1103        }
1104        let out = String::from_utf8(buf).unwrap();
1105        assert!(
1106            out.ends_with("DTM!137|20260101|102~"),
1107            "expected custom delimiters, got {out}"
1108        );
1109    }
1110
1111    #[test]
1112    fn message_writer_counts_write_elements_segments() {
1113        // `MessageWriter` had no mixed-emit delegate, so callers dropped to the
1114        // raw writer and their segments escaped the UNT DE 0074 count.
1115        let mut buf = Vec::new();
1116        {
1117            let mut w = Writer::new(&mut buf);
1118            let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1119            msg.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1120                .unwrap();
1121            msg.write_composites("DTM", &[&["137", "20260101", "102"]])
1122                .unwrap();
1123            msg.finish().unwrap();
1124        }
1125        let out = String::from_utf8(buf).unwrap();
1126        // UNH + NAD + DTM + UNT == 4
1127        assert!(out.contains("UNT+4+1'"), "expected UNT+4, got {out}");
1128    }
1129
1130    #[test]
1131    fn write_and_parse_simple_segment() {
1132        let segs: Vec<Segment<'static>> = vec![Segment::new(
1133            "BGM",
1134            vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
1135        )];
1136        let bytes = crate::segments_to_bytes(&segs).unwrap();
1137        let s = std::str::from_utf8(&bytes).unwrap();
1138        assert!(s.starts_with("BGM+220+ORDER123'"));
1139    }
1140
1141    #[test]
1142    fn release_char_escaped() {
1143        let segs: Vec<Segment<'static>> = vec![Segment::new(
1144            "FTX",
1145            vec![Element::of(&["value+with+delimiters"])],
1146        )];
1147        let bytes = crate::segments_to_bytes(&segs).unwrap();
1148        let s = std::str::from_utf8(&bytes).unwrap();
1149        // The `+` in the value must be escaped as `?+`
1150        assert!(s.contains("?+"), "escape missing: {s}");
1151    }
1152
1153    #[test]
1154    fn round_trip_preserves_values() {
1155        let segs: Vec<Segment<'static>> = vec![
1156            Segment::new(
1157                "UNB",
1158                vec![
1159                    Element::of(&["UNOA", "1"]),
1160                    Element::of(&["SENDER"]),
1161                    Element::of(&["RECEIVER"]),
1162                ],
1163            ),
1164            Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1165        ];
1166        let bytes = crate::segments_to_bytes(&segs).unwrap();
1167        let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
1168            .expect("round-trip parse failed");
1169        assert_eq!(rt[0].tag, "UNB");
1170        assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
1171        assert_eq!(rt[1].tag, "UNZ");
1172    }
1173
1174    /// Verify that `Writer::with_una` uses the configured delimiters throughout,
1175    /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
1176    /// component separators even with a non-default UNA.
1177    #[test]
1178    fn with_una_non_default_delimiters() {
1179        use crate::tokenizer::ServiceStringAdvice;
1180
1181        // Custom UNA: comp_sep=|  elem_sep=!  esc=?  dec_mark=,  rep_sep=*  seg_term=~
1182        let ssa = ServiceStringAdvice {
1183            component_sep: b'|',
1184            element_sep: b'!',
1185            release_char: b'?',
1186            decimal_mark: b',',
1187            repetition_sep: b'*',
1188            segment_term: b'~',
1189        };
1190
1191        let buf = Vec::new();
1192        let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
1193
1194        // write_segment_parts: pre-split; no hard-coded `:` in element strings
1195        writer
1196            .write_segment_parts(
1197                "BGM",
1198                &[
1199                    vec!["220".to_owned(), "SUB1".to_owned()],
1200                    vec!["PO1".to_owned()],
1201                ],
1202            )
1203            .expect("write failed");
1204
1205        let out = writer.finish().expect("finish failed");
1206        let s = std::str::from_utf8(&out).unwrap();
1207
1208        // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
1209        // The writer also emits a UNA header when with_una is used.
1210        assert!(s.contains("BGM"), "BGM segment missing: {s}");
1211        // Slice after UNA so assertions target segment output, not UNA header bytes.
1212        let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
1213        assert!(
1214            after_una.contains('!'),
1215            "missing element sep in segment: {after_una}"
1216        );
1217        assert!(
1218            after_una.contains('|'),
1219            "missing component sep in segment: {after_una}"
1220        );
1221        assert!(
1222            after_una.ends_with('~'),
1223            "missing segment term in segment: {after_una}"
1224        );
1225        // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
1226        assert!(s.contains(','), "missing decimal mark in UNA: {s}");
1227        assert!(!s.contains('+'), "default element sep leaked: {s}");
1228        assert!(!s.contains(':'), "default component sep leaked: {s}");
1229        // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
1230        assert!(
1231            !after_una.contains('\''),
1232            "default segment term leaked after UNA: {after_una}"
1233        );
1234    }
1235}