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/// # What it will not write
206///
207/// Everything the writer emits reparses. Two cases are refused before a byte
208/// reaches the sink, so a rejected segment leaves nothing half-written:
209///
210/// - A segment tag that is not three ASCII uppercase letters
211///   ([`EdifactError::InvalidSegmentTag`]). A tag is written verbatim — EDIFACT
212///   has no way to escape one — so `bgm`, `BGMX`, or `B+M` would produce bytes
213///   that do not read back as the segment they came from.
214/// - A repeating data element when no repetition separator is declared
215///   ([`EdifactError::RepetitionSeparatorNotDeclared`]).
216///
217/// Delimiters *inside a value* are not a problem: those are release-escaped.
218///
219/// # Wrap unbuffered sinks
220///
221/// `Writer` issues a separate write for each tag, delimiter, and value chunk, so
222/// a segment costs roughly one write per component. Against an in-memory
223/// `Vec<u8>` that is free, but against a [`File`][std::fs::File] or a socket each
224/// one is a syscall.
225///
226/// The writer deliberately does **not** buffer internally: an internal buffer
227/// would silently discard everything not yet flushed if the writer were dropped
228/// without [`finish`][Self::finish]. Wrap the sink instead, which makes the
229/// buffering visible and keeps the flush contract in one place:
230///
231/// ```rust
232/// use std::io::BufWriter;
233/// use edifact_rs::Writer;
234///
235/// let sink = Vec::new(); // stands in for a File or TcpStream
236/// let mut writer = Writer::new(BufWriter::new(sink));
237/// writer.write_simple("BGM", &["220"])?;
238/// // `finish` flushes the `Writer` and hands the `BufWriter` back.
239/// let buffered = writer.finish()?;
240/// assert_eq!(buffered.into_inner().unwrap(), b"BGM+220'".to_vec());
241/// # Ok::<(), edifact_rs::EdifactError>(())
242/// ```
243pub struct Writer<W: Write> {
244    inner: W,
245    ssa: ServiceStringAdvice,
246    /// Running count of segments written.  `u64` to prevent silent overflow on
247    /// pathological inputs (a `u32` would wrap after ~4 billion segments).
248    segment_count: u64,
249    /// `segment_count` as of the most recent `UNH`, used by [`Writer::finish_unt`]
250    /// to derive a per-message DE 0074 rather than a writer-lifetime total.
251    message_start_count: u64,
252    /// Whether the segment currently being written incrementally (via the
253    /// event-emitter path) is a `UNH`.  The whole-segment methods pass the tag
254    /// to `end_segment` directly; the emitter only sees it at `StartSegment`.
255    open_segment_is_unh: bool,
256    /// Repertoire every value is encoded into, when the writer is bound to one.
257    ///
258    /// `None` emits UTF-8 unchecked, which is correct for `UNOY` and for any
259    /// payload that happens to be ASCII.
260    charset: Option<Charset>,
261}
262
263/// Return the offset of the first byte in `hay` that must be release-escaped.
264///
265/// The escape set is the four splitting delimiters plus the repetition separator
266/// when the active UNA declares one.  A space at UNA position 7 is the
267/// conventional "not used" sentinel and is never escaped.
268#[inline]
269fn find_escape(ssa: &ServiceStringAdvice, hay: &[u8]) -> Option<usize> {
270    let first = memchr::memchr3(ssa.element_sep, ssa.component_sep, ssa.release_char, hay);
271    let second = if ssa.repetition_sep == b' ' {
272        memchr::memchr(ssa.segment_term, hay)
273    } else {
274        memchr::memchr2(ssa.segment_term, ssa.repetition_sep, hay)
275    };
276    match (first, second) {
277        (None, None) => None,
278        (Some(a), None) => Some(a),
279        (None, Some(b)) => Some(b),
280        (Some(a), Some(b)) => Some(a.min(b)),
281    }
282}
283
284impl<W: Write> Writer<W> {
285    /// Write the segment tag, refusing one the parser would not read back.
286    ///
287    /// A tag is emitted verbatim — EDIFACT has no way to escape it — so a
288    /// lowercase, mis-sized, or delimiter-bearing tag produces bytes that do not
289    /// reparse as the segment they came from. Checked *before* the first byte
290    /// reaches the sink, so a refused segment leaves nothing behind it.
291    ///
292    /// Applies the same predicate as the parser, so the two cannot drift.
293    #[inline]
294    fn write_tag(&mut self, tag: &str) -> Result<(), EdifactError> {
295        if !crate::tokenizer::is_valid_segment_tag(tag) {
296            return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
297        }
298        self.inner.write_all(tag.as_bytes())?;
299        Ok(())
300    }
301
302    /// Create a new writer with default EDIFACT delimiters.
303    pub fn new(inner: W) -> Self {
304        Self {
305            inner,
306            ssa: ServiceStringAdvice::default(),
307            segment_count: 0,
308            message_start_count: 0,
309            open_segment_is_unh: false,
310            charset: None,
311        }
312    }
313
314    /// Bind this writer to a character repertoire.
315    ///
316    /// Every value is then encoded into `charset` rather than emitted as UTF-8,
317    /// and a character the repertoire cannot carry is rejected with
318    /// [`EdifactError::CharacterNotInRepertoire`] instead of being written as
319    /// bytes the receiver decodes as something else.
320    ///
321    /// This is the write-side counterpart of
322    /// [`decode_interchange`][crate::decode_interchange]: a `UNOC` interchange
323    /// must go out as ISO 8859-1, not UTF-8, or `ü` arrives as two mojibake
324    /// characters.
325    ///
326    /// # Example
327    ///
328    /// ```
329    /// use edifact_rs::{Charset, Writer};
330    ///
331    /// let mut writer = Writer::new(Vec::new()).with_charset(Charset::UnoC);
332    /// writer.write_composites("NAD", &[&["BY"], &["Müller"]])?;
333    /// // `ü` goes out as the single Latin-1 byte 0xFC.
334    /// assert_eq!(writer.finish()?, b"NAD+BY+M\xFCller'".to_vec());
335    /// # Ok::<(), edifact_rs::EdifactError>(())
336    /// ```
337    ///
338    /// A value outside the repertoire is refused:
339    ///
340    /// ```
341    /// use edifact_rs::{Charset, EdifactError, Writer};
342    ///
343    /// let mut writer = Writer::new(Vec::new()).with_charset(Charset::UnoA);
344    /// // Level A is upper-case only.
345    /// let err = writer.write_composites("NAD", &[&["BY"], &["Müller"]]).unwrap_err();
346    /// assert!(matches!(err, EdifactError::CharacterNotInRepertoire { .. }));
347    /// ```
348    #[must_use]
349    pub fn with_charset(mut self, charset: Charset) -> Self {
350        self.charset = Some(charset);
351        self
352    }
353
354    /// The repertoire this writer encodes into, if it is bound to one.
355    #[must_use]
356    pub fn charset(&self) -> Option<Charset> {
357        self.charset
358    }
359
360    /// Create a writer that uses `ssa`'s delimiters **without** emitting a `UNA`.
361    ///
362    /// For replying on an inbound interchange's delimiters, or round-tripping a
363    /// syntax-version-4 interchange that has repeating elements but no `UNA`:
364    /// the writer needs the repetition separator, and
365    /// [`with_una`][Self::with_una] would add a header the original lacked.
366    ///
367    /// # Errors
368    ///
369    /// [`EdifactError::InvalidUna`] when the service characters are not mutually
370    /// distinct printable non-alphanumeric ASCII — see
371    /// [`ServiceStringAdvice::is_valid`].
372    ///
373    /// # Example
374    ///
375    /// ```
376    /// use edifact_rs::{ServiceStringAdvice, Writer, from_bytes};
377    ///
378    /// // Version 4: `*` separates RFF's two occurrences, with no UNA to say so.
379    /// let input = b"UNB+UNOC:4+S+R+260101:0900+I'RFF+ON:1*ON:2'UNZ+0+I'";
380    /// let segments: Vec<_> = from_bytes(input).collect::<Result<Vec<_>, _>>()?;
381    ///
382    /// let ssa = ServiceStringAdvice::for_syntax_version(Some(4));
383    /// let mut writer = Writer::with_service_string_advice(Vec::new(), ssa)?;
384    /// for segment in &segments {
385    ///     writer.write_segment(segment)?;
386    /// }
387    /// // Byte-for-byte the input, with no UNA invented.
388    /// assert_eq!(writer.finish()?, input.to_vec());
389    /// # Ok::<(), edifact_rs::EdifactError>(())
390    /// ```
391    pub fn with_service_string_advice(
392        inner: W,
393        ssa: ServiceStringAdvice,
394    ) -> Result<Self, EdifactError> {
395        if !ssa.is_valid() {
396            return Err(EdifactError::InvalidUna);
397        }
398        Ok(Self {
399            inner,
400            ssa,
401            segment_count: 0,
402            message_start_count: 0,
403            open_segment_is_unh: false,
404            charset: None,
405        })
406    }
407
408    /// Create a writer with custom delimiters and write a `UNA` segment first.
409    ///
410    /// [`with_service_string_advice`][Self::with_service_string_advice] is the
411    /// same thing without the header.
412    ///
413    /// # Errors
414    ///
415    /// As [`with_service_string_advice`][Self::with_service_string_advice], plus
416    /// any write failure.
417    pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
418        // All five active service characters must be mutually distinct, non-whitespace,
419        // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
420        if !ssa.is_valid() {
421            return Err(EdifactError::InvalidUna);
422        }
423        // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
424        let una = [
425            b'U',
426            b'N',
427            b'A',
428            ssa.component_sep,
429            ssa.element_sep,
430            ssa.decimal_mark,
431            ssa.release_char,
432            ssa.repetition_sep,
433            ssa.segment_term,
434        ];
435        inner.write_all(&una)?;
436        Ok(Self {
437            inner,
438            ssa,
439            segment_count: 0,
440            message_start_count: 0,
441            open_segment_is_unh: false,
442            charset: None,
443        })
444    }
445
446    /// Record the end of a segment: terminator, count, and `UNH` bookkeeping.
447    ///
448    /// Every emit path funnels through here, so
449    /// [`finish_unt`][Self::finish_unt] derives DE 0074 from the current
450    /// message rather than the writer-lifetime total.
451    #[inline]
452    fn end_segment(&mut self, tag: &str) -> Result<(), EdifactError> {
453        self.inner.write_all(&[self.ssa.segment_term])?;
454        if tag == "UNH" {
455            self.message_start_count = self.segment_count;
456        }
457        self.segment_count += 1;
458        Ok(())
459    }
460
461    /// Write a single segment, including any ISO 9735-4 repetitions.
462    ///
463    /// # Errors
464    ///
465    /// Returns [`EdifactError::RepetitionSeparatorNotDeclared`] when the segment
466    /// carries a repeating data element but the active service string advice
467    /// declares no repetition separator.  The check runs **before** any byte is
468    /// written, so a rejected segment leaves nothing behind in the sink — a
469    /// half-written `RFF+` would otherwise corrupt the interchange for every
470    /// caller that recovers from the error and carries on.
471    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
472        // The sentinel at UNA position 7 is a space.  Emitting it as a separator
473        // would produce output that reads back as a single occurrence whose
474        // value contains a space — corrupt, and quietly so.  Refusing is the
475        // only honest option, and refusing before the first write is the only
476        // one that keeps the sink consistent.
477        if !self.ssa.is_repetition_active() && seg.elements.iter().any(|e| !e.repeats.is_empty()) {
478            return Err(EdifactError::RepetitionSeparatorNotDeclared);
479        }
480
481        self.write_tag(seg.tag())?;
482
483        for element in &seg.elements {
484            self.inner.write_all(&[self.ssa.element_sep])?;
485            for (repetition, components) in element.repetitions().enumerate() {
486                if repetition > 0 {
487                    self.inner.write_all(&[self.ssa.repetition_sep])?;
488                }
489                for (i, (component, _)) in components.iter().enumerate() {
490                    if i > 0 {
491                        self.inner.write_all(&[self.ssa.component_sep])?;
492                    }
493                    self.write_escaped(component)?;
494                }
495            }
496        }
497
498        self.end_segment(seg.tag())
499    }
500
501    /// Write a segment whose data elements are all **simple** — one value each.
502    ///
503    /// The shorthand for the commonest segment shape. Each string is one whole
504    /// data element: a component separator inside a value is escaped as data,
505    /// not promoted to a component boundary, so the output is correct whatever
506    /// delimiters the writer uses.
507    ///
508    /// Reach for [`write_composites`][Self::write_composites] when the elements
509    /// have components, or [`write_elements`][Self::write_elements] when the
510    /// segment mixes the two shapes.
511    ///
512    /// # Example
513    ///
514    /// ```
515    /// use edifact_rs::Writer;
516    ///
517    /// let mut w = Writer::new(Vec::new());
518    /// w.write_simple("BGM", &["220", "PO-4711", "9"])?;
519    /// // A literal `:` stays inside the value it belongs to.
520    /// w.write_simple("FTX", &["AAA", "ACME:INC"])?;
521    /// assert_eq!(w.finish()?, b"BGM+220+PO-4711+9'FTX+AAA+ACME?:INC'".to_vec());
522    /// # Ok::<(), edifact_rs::EdifactError>(())
523    /// ```
524    ///
525    /// # Errors
526    ///
527    /// Returns [`EdifactError`] if the underlying writer fails, or if a value
528    /// cannot be encoded in this writer's [`Charset`].
529    pub fn write_simple<S: AsRef<str>>(
530        &mut self,
531        tag: &str,
532        elements: &[S],
533    ) -> Result<(), EdifactError> {
534        self.write_tag(tag)?;
535        for element in elements {
536            self.inner.write_all(&[self.ssa.element_sep])?;
537            self.write_escaped(element.as_ref())?;
538        }
539        self.end_segment(tag)
540    }
541
542    /// Write a segment whose data elements are all **composite** — a list of
543    /// components each.
544    ///
545    /// Component boundaries are given explicitly rather than inferred by
546    /// splitting, so a value containing the active component separator is
547    /// escaped instead of being silently reinterpreted as a boundary.
548    ///
549    /// The bounds accept borrowed and owned data alike — `&[&[&str]]`,
550    /// `&[Vec<String>]`, `&[[String; 3]]` — so runtime-built segments need no
551    /// conversion. A one-component element is a simple data element, which is
552    /// what makes this the general all-elements form;
553    /// [`write_elements`][Self::write_elements] is the shorthand for the mixed
554    /// shape and [`write_simple`][Self::write_simple] for the all-simple one.
555    ///
556    /// # Example
557    ///
558    /// ```
559    /// use edifact_rs::Writer;
560    ///
561    /// let mut w = Writer::new(Vec::new());
562    /// // Borrowed literals …
563    /// w.write_composites("NAD", &[&["MS"][..], &["ACME:INC"][..]])?;
564    /// // … and owned, runtime-built data, through the same call.
565    /// let dtm = vec![vec!["137".to_string(), "20260101".to_string()]];
566    /// w.write_composites("DTM", &dtm)?;
567    /// assert_eq!(w.finish()?, b"NAD+MS+ACME?:INC'DTM+137:20260101'".to_vec());
568    /// # Ok::<(), edifact_rs::EdifactError>(())
569    /// ```
570    ///
571    /// # Errors
572    ///
573    /// Returns [`EdifactError`] if the underlying writer fails, or if a value
574    /// cannot be encoded in this writer's [`Charset`].
575    pub fn write_composites<E, S>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
576    where
577        E: AsRef<[S]>,
578        S: AsRef<str>,
579    {
580        self.write_tag(tag)?;
581        for element in elements {
582            self.inner.write_all(&[self.ssa.element_sep])?;
583            for (i, comp) in element.as_ref().iter().enumerate() {
584                if i > 0 {
585                    self.inner.write_all(&[self.ssa.component_sep])?;
586                }
587                self.write_escaped(comp.as_ref())?;
588            }
589        }
590        self.end_segment(tag)
591    }
592
593    /// Write a segment whose data elements mix simple and composite shapes.
594    ///
595    /// This is the general form of segment emission and the one that matches
596    /// how EDIFACT segments are actually specified: `NAD` takes a simple
597    /// qualifier followed by a composite party identification, `DTM` takes a
598    /// single composite.  [`write_simple`][Self::write_simple] and
599    /// [`write_composites`][Self::write_composites] are the uniform special
600    /// cases.
601    ///
602    /// Component boundaries are explicit, so a value containing the active
603    /// component separator is escaped rather than silently promoted to a
604    /// boundary.  Nothing is allocated.
605    ///
606    /// # Example
607    ///
608    /// ```rust
609    /// use edifact_rs::{DataElement, Writer, elements};
610    ///
611    /// let mut w = Writer::new(Vec::new());
612    /// // Explicit form …
613    /// w.write_elements(
614    ///     "NAD",
615    ///     &[DataElement::Simple("MS"), DataElement::Composite(&["ACME:INC", "", "9"])],
616    /// )?;
617    /// // … or the `elements!` shorthand.
618    /// w.write_elements("DTM", elements![["137", "20260101", "102"]])?;
619    /// assert_eq!(
620    ///     w.finish()?,
621    ///     b"NAD+MS+ACME?:INC::9'DTM+137:20260101:102'".to_vec(),
622    /// );
623    /// # Ok::<(), edifact_rs::EdifactError>(())
624    /// ```
625    ///
626    /// # Errors
627    ///
628    /// Returns [`EdifactError`] if the underlying writer fails.
629    pub fn write_elements(
630        &mut self,
631        tag: &str,
632        elements: &[DataElement<'_>],
633    ) -> Result<(), EdifactError> {
634        self.write_tag(tag)?;
635        for element in elements {
636            self.inner.write_all(&[self.ssa.element_sep])?;
637            for (i, comp) in element.components().iter().enumerate() {
638                if i > 0 {
639                    self.inner.write_all(&[self.ssa.component_sep])?;
640                }
641                self.write_escaped(comp)?;
642            }
643        }
644        self.end_segment(tag)
645    }
646
647    /// Flush and return the underlying writer.
648    pub fn finish(mut self) -> Result<W, EdifactError> {
649        self.inner.flush()?;
650        Ok(self.inner)
651    }
652
653    /// Write the `UNT` segment and return the inner writer.
654    ///
655    /// The count written into `UNT` DE 0074 covers the current message only:
656    /// `UNH`, every segment written since it, and `UNT` itself.  Segments written
657    /// before the message's `UNH` — an interchange-level `UNB`, or a preceding
658    /// message — are excluded, as EDIFACT requires.
659    ///
660    /// If no `UNH` has been written, the count falls back to every segment
661    /// written so far plus one.
662    ///
663    /// # Errors
664    ///
665    /// Returns an error if writing fails.  Do **not** call [`write_simple`][Self::write_simple] or
666    /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
667    pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
668        // DE 0074 counts UNH + content + UNT.  `message_start_count` is the
669        // absolute segment count immediately after UNH, so content is
670        // `segment_count - message_start_count` and the total adds UNH and UNT.
671        let count = self.segment_count - self.message_start_count + 1;
672        let count_str = count.to_string();
673        self.write_composites("UNT", &[&[count_str.as_str()], &[message_ref]])?;
674        self.finish()
675    }
676
677    /// Returns the total number of segments written so far.
678    pub fn segment_count(&self) -> u64 {
679        self.segment_count
680    }
681
682    /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
683    pub fn service_string_advice(&self) -> ServiceStringAdvice {
684        self.ssa
685    }
686
687    /// Escape a value string for inclusion in an EDIFACT segment.
688    ///
689    /// Any character in `value` that matches the active element separator,
690    /// component separator, release character, or segment terminator is escaped
691    /// by prefixing it with the release character (default `?`).
692    ///
693    /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
694    /// avoiding an allocation on the fast path.
695    ///
696    /// # Example
697    ///
698    /// ```rust,ignore
699    /// let writer = Writer::new(std::io::sink());
700    /// // '+' must be escaped since it is the default element separator.
701    /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
702    /// ```
703    pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
704        let bytes = value.as_bytes();
705        if find_escape(&self.ssa, bytes).is_none() {
706            return Cow::Borrowed(value);
707        }
708        // Built as a `String` from the start.  Assembling a `Vec<u8>` and then
709        // re-validating it needed a fallible conversion whose failure branch was
710        // unreachable, which is exactly the kind of `expect` that has no business
711        // in a library.  Every delimiter is single-byte ASCII (enforced by
712        // `ServiceStringAdvice::is_valid`), so each hit lands on a character
713        // boundary and both halves of the split are valid `&str`.
714        let release = self.ssa.release_char as char;
715        let mut out = String::with_capacity(value.len() + 4);
716        let mut last = 0;
717        while let Some(hit) = find_escape(&self.ssa, &bytes[last..]) {
718            let abs = last + hit;
719            out.push_str(&value[last..abs]);
720            out.push(release);
721            out.push(bytes[abs] as char);
722            last = abs + 1;
723        }
724        out.push_str(&value[last..]);
725        Cow::Owned(out)
726    }
727    /// Write only the segment tag bytes — no element separator or terminator.
728    ///
729    /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
730    #[inline]
731    pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
732        self.write_tag(tag)?;
733        self.open_segment_is_unh = tag == "UNH";
734        Ok(())
735    }
736
737    /// Write one element separator byte.
738    #[inline]
739    pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
740        self.inner.write_all(&[self.ssa.element_sep])?;
741        Ok(())
742    }
743
744    /// Write one component separator byte.
745    #[inline]
746    pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
747        self.inner.write_all(&[self.ssa.component_sep])?;
748        Ok(())
749    }
750
751    /// Write one repetition separator byte, or refuse when none is declared.
752    ///
753    /// Emitting the space sentinel would produce output that reads back as a
754    /// single occurrence whose value contains a space — corrupt, and quietly so.
755    #[inline]
756    pub(crate) fn write_repetition_sep(&mut self) -> Result<(), EdifactError> {
757        if !self.ssa.is_repetition_active() {
758            return Err(EdifactError::RepetitionSeparatorNotDeclared);
759        }
760        self.inner.write_all(&[self.ssa.repetition_sep])?;
761        Ok(())
762    }
763
764    /// Write the segment terminator and increment the internal segment counter.
765    #[inline]
766    pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
767        let tag = if self.open_segment_is_unh { "UNH" } else { "" };
768        self.open_segment_is_unh = false;
769        self.end_segment(tag)
770    }
771
772    /// Write text, encoding it into the bound repertoire when there is one.
773    ///
774    /// Callers must only pass slices that start and end on a character boundary.
775    /// Every delimiter is single-byte ASCII (enforced by
776    /// [`ServiceStringAdvice::is_valid`]), so splitting a value at a delimiter
777    /// always satisfies that.
778    #[inline]
779    fn write_text(&mut self, text: &str) -> Result<(), EdifactError> {
780        match self.charset {
781            None => self.inner.write_all(text.as_bytes())?,
782            Some(charset) => self.inner.write_all(&charset.encode(text)?)?,
783        }
784        Ok(())
785    }
786
787    /// Write a value, escaping any delimiter characters.
788    pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
789        let release = self.ssa.release_char;
790        let bytes = value.as_bytes();
791        let mut last = 0;
792        let mut pos = 0;
793        while pos < bytes.len() {
794            let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
795                break;
796            };
797            let abs = pos + hit;
798            if abs > last {
799                self.write_text(&value[last..abs])?;
800            }
801            // The escaped byte is a service character, hence ASCII in every
802            // repertoire — it needs no encoding pass.
803            self.inner.write_all(&[release, bytes[abs]])?;
804            last = abs + 1;
805            pos = abs + 1;
806        }
807        self.write_text(&value[last..])
808    }
809
810    // ── Interchange envelope helpers ──────────────────────────────────────────
811
812    /// Write a `UNB` interchange header segment.
813    ///
814    /// Generates:
815    /// ```text
816    /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
817    /// ```
818    ///
819    /// Composite components (S001 syntax identifier/version, S004 date/time) are
820    /// passed separately rather than pre-joined with `:`, so they are written
821    /// with the writer's *active* component separator and so a literal separator
822    /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
823    /// silently promoted to a component boundary.
824    ///
825    /// Track the `control_ref` — it must be repeated in the matching
826    /// [`end_interchange`](Self::end_interchange) call.
827    ///
828    /// # Example
829    ///
830    /// ```
831    /// use edifact_rs::Writer;
832    /// let mut w = Writer::new(Vec::new());
833    /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
834    /// assert_eq!(
835    ///     w.finish()?,
836    ///     b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
837    /// );
838    /// # Ok::<(), edifact_rs::EdifactError>(())
839    /// ```
840    ///
841    /// # Errors
842    ///
843    /// Returns [`EdifactError`] if writing fails.
844    #[allow(clippy::too_many_arguments)]
845    pub fn begin_interchange(
846        &mut self,
847        syntax_id: &str,
848        syntax_version: &str,
849        sender: &str,
850        recipient: &str,
851        date: &str,
852        time: &str,
853        control_ref: &str,
854    ) -> Result<(), EdifactError> {
855        // A header that names one repertoire while the body is encoded in another
856        // is the exact silent-corruption failure `with_charset` exists to stop, so
857        // a mismatch is refused rather than written.
858        if let Some(charset) = self.charset {
859            if charset.syntax_identifier() != syntax_id {
860                return Err(EdifactError::CharacterRepertoireMismatch {
861                    declared: syntax_id.to_owned(),
862                    writer: charset.syntax_identifier(),
863                });
864            }
865        }
866        self.write_composites(
867            "UNB",
868            &[
869                &[syntax_id, syntax_version][..],
870                &[sender][..],
871                &[recipient][..],
872                &[date, time][..],
873                &[control_ref][..],
874            ],
875        )
876    }
877
878    /// Write a `UNH` message header and return a [`MessageWriter`] guard.
879    ///
880    /// The guard tracks the per-message segment count automatically.  Call
881    /// [`MessageWriter::finish`] when all message segments have been written — this
882    /// writes the matching `UNT` segment with the correct count.  If `finish` is not
883    /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
884    /// are silently discarded on drop; prefer explicit `finish`).
885    ///
886    /// Generates:
887    /// ```text
888    /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
889    /// ```
890    ///
891    /// # Errors
892    ///
893    /// Returns [`EdifactError`] if writing the `UNH` segment fails.
894    pub fn begin_message<'w>(
895        &'w mut self,
896        message_ref: &str,
897        message_type: &str,
898        version: &str,
899        release: &str,
900        controlling_agency: &str,
901    ) -> Result<MessageWriter<'w, W>, EdifactError> {
902        // Build S009 as an explicit composite.  Formatting it with a literal `:`
903        // and handing it to `write_simple` produced a single collapsed component
904        // whenever the writer used a non-default component separator.
905        self.write_composites(
906            "UNH",
907            &[
908                &[message_ref][..],
909                &[message_type, version, release, controlling_agency][..],
910            ],
911        )?;
912        // Capture `segment_count` after writing UNH so `MessageWriter` knows
913        // the absolute count that includes UNH.
914        let unh_count = self.segment_count;
915        Ok(MessageWriter {
916            writer: self,
917            message_ref: message_ref.to_owned(),
918            unh_count,
919            finished: false,
920        })
921    }
922
923    /// Write a `UNZ` interchange trailer segment.
924    ///
925    /// `message_count` is the number of `UNH`/`UNT` message pairs in the
926    /// interchange.  `control_ref` must match the value passed to
927    /// [`begin_interchange`](Self::begin_interchange).
928    ///
929    /// If you used [`begin_message`](Self::begin_message) for every message in the
930    /// interchange, `message_count` equals the number of times you called that
931    /// method.
932    ///
933    /// # Errors
934    ///
935    /// Returns [`EdifactError`] if writing fails.
936    pub fn end_interchange(
937        &mut self,
938        message_count: u32,
939        control_ref: &str,
940    ) -> Result<(), EdifactError> {
941        let msg_count_str = message_count.to_string();
942        self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
943    }
944}
945
946/// RAII guard for a single EDIFACT message within an interchange.
947///
948/// Obtained from [`Writer::begin_message`].  Writes `UNH` on creation and
949/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
950/// is called or the guard is dropped.
951///
952/// Always prefer calling [`finish`](Self::finish) explicitly so that write
953/// errors can be propagated.  The `Drop` impl writes `UNT` as a best-effort
954/// fallback but silently discards I/O errors.
955///
956/// # Example
957///
958/// ```rust,no_run
959/// # use edifact_rs::{Writer, Segment};
960/// # fn example() -> Result<(), edifact_rs::EdifactError> {
961/// let mut writer = Writer::new(Vec::new());
962/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
963/// {
964///     let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
965///     msg.write_simple("BGM", &["220", "PO001", "9"])?;
966///     msg.finish()?;
967/// }
968/// writer.end_interchange(1, "1")?;
969/// # Ok(())
970/// # }
971/// ```
972pub struct MessageWriter<'w, W: Write> {
973    writer: &'w mut Writer<W>,
974    message_ref: String,
975    /// Absolute segment count immediately after `UNH` was written.
976    unh_count: u64,
977    /// Set to `true` once `finish()` has been called to prevent a double-write
978    /// from the `Drop` impl.
979    finished: bool,
980}
981
982impl<W: Write> MessageWriter<'_, W> {
983    /// Write an all-simple segment within this message.
984    ///
985    /// Delegates to [`Writer::write_simple`].
986    ///
987    /// # Errors
988    ///
989    /// Returns [`EdifactError`] if the underlying writer fails.
990    pub fn write_simple<S: AsRef<str>>(
991        &mut self,
992        tag: &str,
993        elements: &[S],
994    ) -> Result<(), EdifactError> {
995        self.writer.write_simple(tag, elements)
996    }
997
998    /// Write an all-composite segment within this message.
999    ///
1000    /// Delegates to [`Writer::write_composites`].
1001    ///
1002    /// # Errors
1003    ///
1004    /// Returns [`EdifactError`] if the underlying writer fails.
1005    pub fn write_composites<E, S>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
1006    where
1007        E: AsRef<[S]>,
1008        S: AsRef<str>,
1009    {
1010        self.writer.write_composites(tag, elements)
1011    }
1012
1013    /// Write a segment mixing simple and composite data elements within this message.
1014    ///
1015    /// Delegates to [`Writer::write_elements`].
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns [`EdifactError`] if the underlying writer fails.
1020    pub fn write_elements(
1021        &mut self,
1022        tag: &str,
1023        elements: &[DataElement<'_>],
1024    ) -> Result<(), EdifactError> {
1025        self.writer.write_elements(tag, elements)
1026    }
1027
1028    /// Write a fully-typed segment within this message.
1029    ///
1030    /// Delegates to [`Writer::write_segment`].
1031    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
1032        self.writer.write_segment(seg)
1033    }
1034
1035    /// Compute the per-message segment count and write `UNT`, consuming the guard.
1036    ///
1037    /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
1038    /// and `UNT` itself — matching the EDIFACT standard.
1039    ///
1040    /// # Errors
1041    ///
1042    /// Returns [`EdifactError`] if writing the `UNT` segment fails.
1043    pub fn finish(mut self) -> Result<(), EdifactError> {
1044        self.write_unt()?;
1045        self.finished = true;
1046        Ok(())
1047    }
1048
1049    fn write_unt(&mut self) -> Result<(), EdifactError> {
1050        // Segments since UNH: writer.segment_count - unh_count (content only).
1051        // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
1052        let count = self.writer.segment_count - self.unh_count + 2;
1053        let count_str = count.to_string();
1054        self.writer.write_composites(
1055            "UNT",
1056            &[&[count_str.as_str()], &[self.message_ref.as_str()]],
1057        )
1058    }
1059}
1060
1061impl<W: Write> Drop for MessageWriter<'_, W> {
1062    fn drop(&mut self) {
1063        if !self.finished {
1064            // Best-effort: write UNT; errors cannot be propagated from drop.
1065            let _ = self.write_unt();
1066        }
1067    }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073    use crate::model::Element;
1074
1075    /// A non-default UNA whose delimiters share no byte with the defaults.
1076    fn exotic_ssa() -> ServiceStringAdvice {
1077        ServiceStringAdvice {
1078            component_sep: b'|',
1079            element_sep: b'!',
1080            decimal_mark: b',',
1081            release_char: b'#',
1082            repetition_sep: b'*',
1083            segment_term: b'~',
1084        }
1085    }
1086
1087    #[test]
1088    fn unh_composite_uses_the_active_component_separator() {
1089        // S009 must be built as a real composite: a literal `:` in a
1090        // `format!` collapses into one component under a custom UNA.
1091        let mut buf = Vec::new();
1092        {
1093            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1094            let msg = w
1095                .begin_message("1", "ORDERS", "D", "96A", "UN")
1096                .expect("UNH");
1097            msg.finish().expect("UNT");
1098        }
1099        let out = String::from_utf8(buf).unwrap();
1100        assert!(
1101            out.contains("UNH!1!ORDERS|D|96A|UN~"),
1102            "S009 must use `|`, got {out}"
1103        );
1104    }
1105
1106    #[test]
1107    fn round_trips_through_a_custom_una() {
1108        // The library must be able to re-read its own output verbatim.
1109        let mut buf = Vec::new();
1110        {
1111            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1112            w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
1113                .unwrap();
1114            let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1115            msg.write_simple("BGM", &["220"]).unwrap();
1116            msg.finish().unwrap();
1117            w.end_interchange(1, "IC1").unwrap();
1118        }
1119        let segs: Vec<_> = crate::from_bytes(&buf)
1120            .collect::<Result<Vec<_>, _>>()
1121            .expect("own output must reparse");
1122        let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
1123        assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
1124        assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
1125        crate::validate_envelope(&segs).expect("own output must pass envelope validation");
1126    }
1127
1128    #[test]
1129    fn finish_unt_counts_only_the_current_message() {
1130        // `finish_unt` used the writer-lifetime segment total, so a preceding
1131        // UNB inflated DE 0074 and the interchange failed its own validation.
1132        let mut buf = Vec::new();
1133        {
1134            let mut w = Writer::new(&mut buf);
1135            w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1136                .unwrap();
1137            w.write_composites("UNH", &[&["1"][..], &["ORDERS", "D", "96A", "UN"][..]])
1138                .unwrap();
1139            w.write_simple("BGM", &["220"]).unwrap();
1140            w.finish_unt("1").unwrap();
1141        }
1142        let out = String::from_utf8(buf).unwrap();
1143        // UNH + BGM + UNT == 3
1144        assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1145    }
1146
1147    #[test]
1148    fn repetition_separator_is_escaped_when_declared() {
1149        let mut buf = Vec::new();
1150        {
1151            let mut w = Writer::with_una(
1152                &mut buf,
1153                ServiceStringAdvice {
1154                    repetition_sep: b'*',
1155                    ..ServiceStringAdvice::default()
1156                },
1157            )
1158            .unwrap();
1159            w.write_composites("FTX", &[&["a*b"]]).unwrap();
1160        }
1161        let out = String::from_utf8(buf).unwrap();
1162        assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
1163    }
1164
1165    #[test]
1166    fn repetition_separator_sentinel_is_not_escaped() {
1167        // Space at UNA position 7 means "not used" and must never be escaped.
1168        let w = Writer::new(std::io::sink());
1169        assert_eq!(w.escape_value("a b"), "a b");
1170    }
1171
1172    #[test]
1173    fn write_composites_escapes_a_literal_component_separator() {
1174        let mut buf = Vec::new();
1175        {
1176            let mut w = Writer::new(&mut buf);
1177            w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
1178                .unwrap();
1179        }
1180        let segs: Vec<_> = crate::from_bytes(&buf)
1181            .collect::<Result<Vec<_>, _>>()
1182            .unwrap();
1183        // The `:` stays inside the value instead of splitting the element.
1184        assert_eq!(
1185            segs[0].get_element(1).unwrap().get_component(0),
1186            Some("ACME:INC")
1187        );
1188    }
1189
1190    #[test]
1191    fn write_elements_mixes_simple_and_composite() {
1192        let mut buf = Vec::new();
1193        {
1194            let mut w = Writer::new(&mut buf);
1195            w.write_elements(
1196                "NAD",
1197                &[
1198                    DataElement::Simple("MS"),
1199                    DataElement::Composite(&["9900112233445", "", "293"]),
1200                ],
1201            )
1202            .unwrap();
1203        }
1204        assert_eq!(buf, b"NAD+MS+9900112233445::293'");
1205    }
1206
1207    #[test]
1208    fn elements_macro_matches_the_explicit_form() {
1209        let mut macro_buf = Vec::new();
1210        {
1211            let mut w = Writer::new(&mut macro_buf);
1212            w.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1213                .unwrap();
1214            w.write_elements("DTM", elements![["137", "20260101", "102"]])
1215                .unwrap();
1216        }
1217        let mut explicit_buf = Vec::new();
1218        {
1219            let mut w = Writer::new(&mut explicit_buf);
1220            w.write_elements(
1221                "NAD",
1222                &[
1223                    DataElement::Simple("MS"),
1224                    DataElement::Composite(&["ACME", "", "9"]),
1225                ],
1226            )
1227            .unwrap();
1228            w.write_elements(
1229                "DTM",
1230                &[DataElement::Composite(&["137", "20260101", "102"])],
1231            )
1232            .unwrap();
1233        }
1234        assert_eq!(macro_buf, explicit_buf);
1235        assert_eq!(macro_buf, b"NAD+MS+ACME::9'DTM+137:20260101:102'");
1236    }
1237
1238    #[test]
1239    fn elements_macro_accepts_arbitrary_expressions() {
1240        // Builders emit runtime values, not literals.  A `tt`-based macro only
1241        // matched single-token entries, so `qualifier.as_str()` failed to parse
1242        // — which is precisely the shape this macro exists for.
1243        let qualifier = String::from("MS");
1244        let gln = "9900112233445";
1245        let dtm: Vec<&str> = vec!["137", "20260101", "102"];
1246
1247        let mut buf = Vec::new();
1248        {
1249            let mut w = Writer::new(&mut buf);
1250            w.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])
1251                .unwrap();
1252            w.write_elements("DTM", elements![dtm]).unwrap();
1253            w.write_elements("FTX", elements![qualifier]).unwrap();
1254            w.write_elements("UNS", elements![]).unwrap();
1255        }
1256        assert_eq!(
1257            String::from_utf8(buf).unwrap(),
1258            "NAD+MS+9900112233445::293'DTM+137:20260101:102'FTX+MS'UNS'"
1259        );
1260    }
1261
1262    #[test]
1263    fn write_elements_escapes_a_literal_component_separator() {
1264        // The `:` stays inside the value instead of splitting the element —
1265        // the failure mode of pre-joining components into one string.
1266        let mut buf = Vec::new();
1267        {
1268            let mut w = Writer::new(&mut buf);
1269            w.write_elements(
1270                "NAD",
1271                &[DataElement::Simple("MS"), DataElement::Simple("ACME:INC")],
1272            )
1273            .unwrap();
1274        }
1275        let segs: Vec<_> = crate::from_bytes(&buf)
1276            .collect::<Result<Vec<_>, _>>()
1277            .unwrap();
1278        assert_eq!(
1279            segs[0].get_element(1).unwrap().get_component(0),
1280            Some("ACME:INC")
1281        );
1282    }
1283
1284    #[test]
1285    fn write_elements_uses_the_active_component_separator() {
1286        let mut buf = Vec::new();
1287        {
1288            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1289            w.write_elements("DTM", elements![["137", "20260101", "102"]])
1290                .unwrap();
1291        }
1292        let out = String::from_utf8(buf).unwrap();
1293        assert!(
1294            out.ends_with("DTM!137|20260101|102~"),
1295            "expected custom delimiters, got {out}"
1296        );
1297    }
1298
1299    #[test]
1300    fn message_writer_counts_write_elements_segments() {
1301        // `MessageWriter` had no mixed-emit delegate, so callers dropped to the
1302        // raw writer and their segments escaped the UNT DE 0074 count.
1303        let mut buf = Vec::new();
1304        {
1305            let mut w = Writer::new(&mut buf);
1306            let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1307            msg.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1308                .unwrap();
1309            msg.write_composites("DTM", &[&["137", "20260101", "102"]])
1310                .unwrap();
1311            msg.finish().unwrap();
1312        }
1313        let out = String::from_utf8(buf).unwrap();
1314        // UNH + NAD + DTM + UNT == 4
1315        assert!(out.contains("UNT+4+1'"), "expected UNT+4, got {out}");
1316    }
1317
1318    #[test]
1319    fn write_segment_records_unh_for_the_unt_count() {
1320        // `write_segment` and `write_composites` did not record the UNH
1321        // marker, so `finish_unt` fell back to the writer-lifetime total and
1322        // DE 0074 came out inflated by every preceding interchange segment.
1323        let mut buf = Vec::new();
1324        {
1325            let mut w = Writer::new(&mut buf);
1326            w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1327                .unwrap();
1328            w.write_segment(&Segment::new(
1329                "UNH",
1330                vec![
1331                    Element::of(&["1"]),
1332                    Element::of(&["ORDERS", "D", "96A", "UN"]),
1333                ],
1334            ))
1335            .unwrap();
1336            w.write_segment(&Segment::new("BGM", vec![Element::of(&["220"])]))
1337                .unwrap();
1338            w.finish_unt("1").unwrap();
1339        }
1340        let out = String::from_utf8(buf).unwrap();
1341        // UNH + BGM + UNT == 3, not 4 (which would count the UNB).
1342        assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1343    }
1344
1345    #[test]
1346    fn write_composites_records_unh_for_the_unt_count() {
1347        let mut buf = Vec::new();
1348        {
1349            let mut w = Writer::new(&mut buf);
1350            w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1351                .unwrap();
1352            w.write_composites(
1353                "UNH",
1354                &[
1355                    vec!["1".to_owned()],
1356                    vec![
1357                        "ORDERS".to_owned(),
1358                        "D".to_owned(),
1359                        "96A".to_owned(),
1360                        "UN".to_owned(),
1361                    ],
1362                ],
1363            )
1364            .unwrap();
1365            w.write_simple("BGM", &["220"]).unwrap();
1366            w.finish_unt("1").unwrap();
1367        }
1368        let out = String::from_utf8(buf).unwrap();
1369        assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1370    }
1371
1372    #[test]
1373    fn escape_value_handles_multi_byte_text() {
1374        // The old implementation assembled a `Vec<u8>` and re-validated it with
1375        // an `expect`.  Escaping around non-ASCII text is the case that made
1376        // that conversion look fallible in the first place.
1377        let w = Writer::new(std::io::sink());
1378        assert_eq!(w.escape_value("Grüße+Köln"), "Grüße?+Köln");
1379        assert_eq!(w.escape_value("Grüße"), "Grüße");
1380        assert!(matches!(w.escape_value("plain"), Cow::Borrowed("plain")));
1381    }
1382
1383    #[test]
1384    fn write_and_parse_simple_segment() {
1385        let segs: Vec<Segment<'static>> = vec![Segment::new(
1386            "BGM",
1387            vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
1388        )];
1389        let bytes = crate::segments_to_bytes(&segs).unwrap();
1390        let s = std::str::from_utf8(&bytes).unwrap();
1391        assert!(s.starts_with("BGM+220+ORDER123'"));
1392    }
1393
1394    #[test]
1395    fn a_tag_the_parser_would_reject_is_never_written() {
1396        // A tag is emitted verbatim — EDIFACT cannot escape one — so writing an
1397        // invalid tag produces bytes that do not reparse as the segment they
1398        // came from.  Every write path must refuse before the first byte.
1399        for tag in ["bgm", "BGMX", "BG", "", "B+M", "B'M", "BG1", "BGÜ"] {
1400            let mut writer = Writer::new(Vec::new());
1401            let err = writer
1402                .write_simple(tag, &["220"])
1403                .expect_err("an invalid tag must be refused");
1404            assert!(
1405                matches!(err, EdifactError::InvalidSegmentTag(ref t) if t == tag),
1406                "expected InvalidSegmentTag({tag:?}), got {err:?}",
1407            );
1408            // Nothing reached the sink.
1409            assert!(
1410                writer.finish().expect("finish").is_empty(),
1411                "a refused segment must leave the sink untouched",
1412            );
1413        }
1414    }
1415
1416    #[test]
1417    fn every_write_path_validates_the_tag() {
1418        let bad = "bgm";
1419        macro_rules! refused {
1420            ($call:expr) => {
1421                assert!(
1422                    matches!($call, Err(EdifactError::InvalidSegmentTag(_))),
1423                    "a write path accepted an invalid tag",
1424                );
1425            };
1426        }
1427
1428        let mut w = Writer::new(Vec::new());
1429        refused!(w.write_simple(bad, &["1"]));
1430        refused!(w.write_composites(bad, &[&["1"][..]]));
1431        refused!(w.write_elements(bad, elements!["1"]));
1432        refused!(w.write_segment(&Segment::new(bad, vec![Element::of(&["1"])])));
1433    }
1434
1435    #[test]
1436    fn whatever_the_writer_emits_the_parser_reads_back() {
1437        // The round-trip property the tag check exists to preserve.
1438        let segments = vec![
1439            Segment::new(
1440                "UNB",
1441                vec![Element::of(&["UNOA", "1"]), Element::of(&["S"])],
1442            ),
1443            Segment::new(
1444                "BGM",
1445                vec![Element::of(&["220"]), Element::of(&["PO?+1'X"])],
1446            ),
1447            Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1448        ];
1449        let bytes = crate::segments_to_bytes(&segments).expect("write");
1450        let reparsed: Vec<_> = crate::from_bytes(&bytes)
1451            .collect::<Result<Vec<_>, _>>()
1452            .expect("everything the writer emits must reparse");
1453
1454        assert_eq!(
1455            reparsed.iter().map(Segment::tag).collect::<Vec<_>>(),
1456            ["UNB", "BGM", "UNZ"],
1457        );
1458        // Delimiters inside a *value* survive, because a value can be escaped.
1459        assert_eq!(reparsed[1].element_str(1), Some("PO?+1'X"));
1460    }
1461
1462    #[test]
1463    fn release_char_escaped() {
1464        let segs: Vec<Segment<'static>> = vec![Segment::new(
1465            "FTX",
1466            vec![Element::of(&["value+with+delimiters"])],
1467        )];
1468        let bytes = crate::segments_to_bytes(&segs).unwrap();
1469        let s = std::str::from_utf8(&bytes).unwrap();
1470        // The `+` in the value must be escaped as `?+`
1471        assert!(s.contains("?+"), "escape missing: {s}");
1472    }
1473
1474    #[test]
1475    fn round_trip_preserves_values() {
1476        let segs: Vec<Segment<'static>> = vec![
1477            Segment::new(
1478                "UNB",
1479                vec![
1480                    Element::of(&["UNOA", "1"]),
1481                    Element::of(&["SENDER"]),
1482                    Element::of(&["RECEIVER"]),
1483                ],
1484            ),
1485            Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1486        ];
1487        let bytes = crate::segments_to_bytes(&segs).unwrap();
1488        let rt: Vec<crate::OwnedSegment> = crate::from_reader(std::io::Cursor::new(&bytes))
1489            .collect::<Result<Vec<_>, _>>()
1490            .expect("round-trip parse failed");
1491        assert_eq!(rt[0].tag, "UNB");
1492        assert_eq!(rt[0].element_str(0), Some("UNOA"));
1493        assert_eq!(rt[1].tag, "UNZ");
1494    }
1495
1496    /// Verify that `Writer::with_una` uses the configured delimiters throughout,
1497    /// and that `write_composites` (the delimiter-agnostic API) produces correct
1498    /// component separators even with a non-default UNA.
1499    #[test]
1500    fn with_una_non_default_delimiters() {
1501        use crate::tokenizer::ServiceStringAdvice;
1502
1503        // Custom UNA: comp_sep=|  elem_sep=!  esc=?  dec_mark=,  rep_sep=*  seg_term=~
1504        let ssa = ServiceStringAdvice {
1505            component_sep: b'|',
1506            element_sep: b'!',
1507            release_char: b'?',
1508            decimal_mark: b',',
1509            repetition_sep: b'*',
1510            segment_term: b'~',
1511        };
1512
1513        let buf = Vec::new();
1514        let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
1515
1516        // write_composites: pre-split; no hard-coded `:` in element strings
1517        writer
1518            .write_composites(
1519                "BGM",
1520                &[
1521                    vec!["220".to_owned(), "SUB1".to_owned()],
1522                    vec!["PO1".to_owned()],
1523                ],
1524            )
1525            .expect("write failed");
1526
1527        let out = writer.finish().expect("finish failed");
1528        let s = std::str::from_utf8(&out).unwrap();
1529
1530        // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
1531        // The writer also emits a UNA header when with_una is used.
1532        assert!(s.contains("BGM"), "BGM segment missing: {s}");
1533        // Slice after UNA so assertions target segment output, not UNA header bytes.
1534        let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
1535        assert!(
1536            after_una.contains('!'),
1537            "missing element sep in segment: {after_una}"
1538        );
1539        assert!(
1540            after_una.contains('|'),
1541            "missing component sep in segment: {after_una}"
1542        );
1543        assert!(
1544            after_una.ends_with('~'),
1545            "missing segment term in segment: {after_una}"
1546        );
1547        // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
1548        assert!(s.contains(','), "missing decimal mark in UNA: {s}");
1549        assert!(!s.contains('+'), "default element sep leaked: {s}");
1550        assert!(!s.contains(':'), "default component sep leaked: {s}");
1551        // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
1552        assert!(
1553            !after_una.contains('\''),
1554            "default segment term leaked after UNA: {after_una}"
1555        );
1556    }
1557}