Skip to main content

edifact_rs/
writer.rs

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