Skip to main content

edifact_rs/
writer.rs

1//! EDIFACT writer — serializes [`Segment`]s to wire format.
2
3use crate::{error::EdifactError, model::Segment, tokenizer::ServiceStringAdvice};
4use std::borrow::Cow;
5use std::io::Write;
6
7/// Streaming EDIFACT writer.
8///
9/// Wraps any [`Write`] implementation and serializes segments one at a time.
10/// Call [`Writer::finish`] to flush and get the underlying writer back.
11pub struct Writer<W: Write> {
12    inner: W,
13    ssa: ServiceStringAdvice,
14    /// Running count of segments written.  `u64` to prevent silent overflow on
15    /// pathological inputs (a `u32` would wrap after ~4 billion segments).
16    segment_count: u64,
17    /// `segment_count` as of the most recent `UNH`, used by [`Writer::finish_unt`]
18    /// to derive a per-message DE 0074 rather than a writer-lifetime total.
19    message_start_count: u64,
20}
21
22/// Return the offset of the first byte in `hay` that must be release-escaped.
23///
24/// The escape set is the four splitting delimiters plus the repetition separator
25/// when the active UNA declares one.  A space at UNA position 7 is the
26/// conventional "not used" sentinel and is never escaped.
27#[inline]
28fn find_escape(ssa: &ServiceStringAdvice, hay: &[u8]) -> Option<usize> {
29    let first = memchr::memchr3(ssa.element_sep, ssa.component_sep, ssa.release_char, hay);
30    let second = if ssa.repetition_sep == b' ' {
31        memchr::memchr(ssa.segment_term, hay)
32    } else {
33        memchr::memchr2(ssa.segment_term, ssa.repetition_sep, hay)
34    };
35    match (first, second) {
36        (None, None) => None,
37        (Some(a), None) => Some(a),
38        (None, Some(b)) => Some(b),
39        (Some(a), Some(b)) => Some(a.min(b)),
40    }
41}
42
43impl<W: Write> Writer<W> {
44    /// Create a new writer with default EDIFACT delimiters.
45    pub fn new(inner: W) -> Self {
46        Self {
47            inner,
48            ssa: ServiceStringAdvice::default(),
49            segment_count: 0,
50            message_start_count: 0,
51        }
52    }
53
54    /// Create a writer with custom delimiters and write a UNA segment first.
55    pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
56        // All five active service characters must be mutually distinct, non-whitespace,
57        // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
58        if !ssa.is_valid() {
59            return Err(EdifactError::InvalidUna);
60        }
61        // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
62        let una = [
63            b'U',
64            b'N',
65            b'A',
66            ssa.component_sep,
67            ssa.element_sep,
68            ssa.decimal_mark,
69            ssa.release_char,
70            ssa.repetition_sep,
71            ssa.segment_term,
72        ];
73        inner.write_all(&una)?;
74        Ok(Self {
75            inner,
76            ssa,
77            segment_count: 0,
78            message_start_count: 0,
79        })
80    }
81
82    /// Write a single segment.
83    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
84        // Tag
85        self.inner.write_all(seg.tag.as_bytes())?;
86
87        for element in &seg.elements {
88            // Element separator
89            self.inner.write_all(&[self.ssa.element_sep])?;
90            let mut first_component = true;
91            for (component, _) in &element.components {
92                if !first_component {
93                    self.inner.write_all(&[self.ssa.component_sep])?;
94                }
95                first_component = false;
96                self.write_escaped(component)?;
97            }
98        }
99
100        // Segment terminator
101        self.inner.write_all(&[self.ssa.segment_term])?;
102        self.segment_count += 1;
103        Ok(())
104    }
105
106    /// Write a raw segment from tag + element string slices.
107    ///
108    /// Each element string is split on the **active component-separator byte** from the
109    /// configured [`ServiceStringAdvice`][crate::ServiceStringAdvice] to identify component
110    /// boundaries.  The default component separator is `:` (0x3A), but this can differ when a
111    /// non-default `UNA` string was used to construct the writer.
112    ///
113    /// # Delimiter dependency
114    ///
115    /// Callers that embed the literal `:` character in element strings rely on `:` being
116    /// the component separator.  When the writer uses a non-default delimiter set, `:` will
117    /// **not** be treated as a component boundary and the segment will be written incorrectly.
118    ///
119    /// **UTF-8 safety**: EDIFACT syntax requires all delimiter bytes to be single-byte ASCII
120    /// characters (values 0x00–0x7F).  Non-ASCII delimiter bytes would bisect multi-byte UTF-8
121    /// sequences in data values and produce malformed output.  All fields of
122    /// [`ServiceStringAdvice`][crate::ServiceStringAdvice] must therefore hold ASCII byte values.
123    ///
124    /// To produce correct output regardless of the active delimiter, prefer
125    /// [`Self::write_segment_parts`] which accepts pre-split component slices.
126    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
127        self.inner.write_all(tag.as_bytes())?;
128        let comp_sep = self.ssa.component_sep;
129        for el in elements {
130            self.inner.write_all(&[self.ssa.element_sep])?;
131            // Byte-level split: EDIFACT delimiters are always single bytes.
132            let mut parts = el.as_bytes().split(|&b| b == comp_sep);
133            if let Some(first) = parts.next() {
134                // INVARIANT: input is valid UTF-8 and we split on a single-byte ASCII
135                // delimiter, so each part remains a valid UTF-8 slice.
136                self.write_escaped(
137                    std::str::from_utf8(first).map_err(|_| EdifactError::InvalidUtf8)?,
138                )?;
139            }
140            for part in parts {
141                self.inner.write_all(&[comp_sep])?;
142                self.write_escaped(
143                    std::str::from_utf8(part).map_err(|_| EdifactError::InvalidUtf8)?,
144                )?;
145            }
146        }
147        self.inner.write_all(&[self.ssa.segment_term])?;
148        if tag == "UNH" {
149            self.message_start_count = self.segment_count;
150        }
151        self.segment_count += 1;
152        Ok(())
153    }
154
155    /// Write a segment from a tag and pre-split element/component data.
156    ///
157    /// `elements` is a slice of elements; each element is a sequence of component strings.
158    /// This avoids the lifetime constraints of [`Self::write_segment`] when building
159    /// segments from runtime-owned data (e.g. inside [`crate::WriterEmitter`]).
160    pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
161    where
162        E: AsRef<[String]>,
163    {
164        self.inner.write_all(tag.as_bytes())?;
165        for element in elements {
166            self.inner.write_all(&[self.ssa.element_sep])?;
167            let mut first = true;
168            for comp in element.as_ref() {
169                if !first {
170                    self.inner.write_all(&[self.ssa.component_sep])?;
171                }
172                first = false;
173                self.write_escaped(comp.as_str())?;
174            }
175        }
176        self.inner.write_all(&[self.ssa.segment_term])?;
177        self.segment_count += 1;
178        Ok(())
179    }
180
181    /// Write a segment from a tag and borrowed element/component slices.
182    ///
183    /// Unlike [`Self::write_raw`], component boundaries are given explicitly
184    /// rather than inferred by splitting on the active component separator, so
185    /// values containing a literal separator byte are escaped instead of being
186    /// silently reinterpreted as a composite boundary.  Unlike
187    /// [`Self::write_segment_parts`], no `String` allocation is required.
188    ///
189    /// # Example
190    ///
191    /// ```
192    /// use edifact_rs::Writer;
193    /// let mut w = Writer::new(Vec::new());
194    /// // The `:` inside the sender id stays part of the value.
195    /// w.write_composites("NAD", &[&["MS"][..], &["ACME:INC"][..]])?;
196    /// assert_eq!(w.finish()?, b"NAD+MS+ACME?:INC'".to_vec());
197    /// # Ok::<(), edifact_rs::EdifactError>(())
198    /// ```
199    ///
200    /// # Errors
201    ///
202    /// Returns [`EdifactError`] if the underlying writer fails.
203    pub fn write_composites(
204        &mut self,
205        tag: &str,
206        elements: &[&[&str]],
207    ) -> Result<(), EdifactError> {
208        self.inner.write_all(tag.as_bytes())?;
209        for element in elements {
210            self.inner.write_all(&[self.ssa.element_sep])?;
211            for (i, comp) in element.iter().enumerate() {
212                if i > 0 {
213                    self.inner.write_all(&[self.ssa.component_sep])?;
214                }
215                self.write_escaped(comp)?;
216            }
217        }
218        self.inner.write_all(&[self.ssa.segment_term])?;
219        if tag == "UNH" {
220            self.message_start_count = self.segment_count;
221        }
222        self.segment_count += 1;
223        Ok(())
224    }
225
226    /// Flush and return the underlying writer.
227    pub fn finish(mut self) -> Result<W, EdifactError> {
228        self.inner.flush()?;
229        Ok(self.inner)
230    }
231
232    /// Write the `UNT` segment and return the inner writer.
233    ///
234    /// The count written into `UNT` DE 0074 covers the current message only:
235    /// `UNH`, every segment written since it, and `UNT` itself.  Segments written
236    /// before the message's `UNH` — an interchange-level `UNB`, or a preceding
237    /// message — are excluded, as EDIFACT requires.
238    ///
239    /// If no `UNH` has been written, the count falls back to every segment
240    /// written so far plus one.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if writing fails.  Do **not** call [`write_raw`][Self::write_raw] or
245    /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
246    pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
247        // DE 0074 counts UNH + content + UNT.  `message_start_count` is the
248        // absolute segment count immediately after UNH, so content is
249        // `segment_count - message_start_count` and the total adds UNH and UNT.
250        let count = self.segment_count - self.message_start_count + 1;
251        let count_str = count.to_string();
252        self.write_composites("UNT", &[&[count_str.as_str()], &[message_ref]])?;
253        self.finish()
254    }
255
256    /// Returns the total number of segments written so far.
257    pub fn segment_count(&self) -> u64 {
258        self.segment_count
259    }
260
261    /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
262    pub fn service_string_advice(&self) -> ServiceStringAdvice {
263        self.ssa
264    }
265
266    /// Escape a value string for inclusion in an EDIFACT segment.
267    ///
268    /// Any character in `value` that matches the active element separator,
269    /// component separator, release character, or segment terminator is escaped
270    /// by prefixing it with the release character (default `?`).
271    ///
272    /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
273    /// avoiding an allocation on the fast path.
274    ///
275    /// # Example
276    ///
277    /// ```rust,ignore
278    /// let writer = Writer::new(std::io::sink());
279    /// // '+' must be escaped since it is the default element separator.
280    /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
281    /// ```
282    pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
283        let release = self.ssa.release_char;
284        let bytes = value.as_bytes();
285        if find_escape(&self.ssa, bytes).is_none() {
286            return Cow::Borrowed(value);
287        }
288        let mut out = Vec::with_capacity(value.len() + 4);
289        let mut last = 0;
290        let mut pos = 0;
291        while pos < bytes.len() {
292            let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
293                break;
294            };
295            let abs = pos + hit;
296            out.extend_from_slice(&bytes[last..abs]);
297            out.push(release);
298            out.push(bytes[abs]);
299            last = abs + 1;
300            pos = abs + 1;
301        }
302        out.extend_from_slice(&bytes[last..]);
303        // SAFETY:
304        //   1. `value` is a valid `&str`, so `bytes` is valid UTF-8 to start.
305        //   2. `self.ssa.release_char` is a single-byte ASCII value (0x21–0x7E),
306        //      enforced at construction time by `ServiceStringAdvice::is_valid()`
307        //      (called in `Writer::with_una`; the default SSA hardcodes `?` = 0x3F).
308        //      Inserting a single ASCII byte cannot split or corrupt a multi-byte
309        //      UTF-8 sequence, because ASCII bytes always have the high bit clear
310        //      while continuation bytes of multi-byte sequences always have the high
311        //      bit set (0x80–0xBF).
312        //   3. All other bytes are copied verbatim from the valid UTF-8 source.
313        Cow::Owned(
314            String::from_utf8(out).expect(
315                "escape_value: output is not valid UTF-8; this is a bug in the escape logic",
316            ),
317        )
318    }
319    /// Write only the segment tag bytes — no element separator or terminator.
320    ///
321    /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
322    #[inline]
323    pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
324        self.inner.write_all(tag.as_bytes())?;
325        Ok(())
326    }
327
328    /// Write one element separator byte.
329    #[inline]
330    pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
331        self.inner.write_all(&[self.ssa.element_sep])?;
332        Ok(())
333    }
334
335    /// Write one component separator byte.
336    #[inline]
337    pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
338        self.inner.write_all(&[self.ssa.component_sep])?;
339        Ok(())
340    }
341
342    /// Write the segment terminator and increment the internal segment counter.
343    #[inline]
344    pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
345        self.inner.write_all(&[self.ssa.segment_term])?;
346        self.segment_count += 1;
347        Ok(())
348    }
349
350    /// Write a value, escaping any delimiter characters.
351    pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
352        let release = self.ssa.release_char;
353        let bytes = value.as_bytes();
354        let mut last = 0;
355        let mut pos = 0;
356        while pos < bytes.len() {
357            let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
358                break;
359            };
360            let abs = pos + hit;
361            if abs > last {
362                self.inner.write_all(&bytes[last..abs])?;
363            }
364            self.inner.write_all(&[release, bytes[abs]])?;
365            last = abs + 1;
366            pos = abs + 1;
367        }
368        self.inner.write_all(&bytes[last..])?;
369        Ok(())
370    }
371
372    // ── Interchange envelope helpers ──────────────────────────────────────────
373
374    /// Write a `UNB` interchange header segment.
375    ///
376    /// Generates:
377    /// ```text
378    /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
379    /// ```
380    ///
381    /// Composite components (S001 syntax identifier/version, S004 date/time) are
382    /// passed separately rather than pre-joined with `:`, so they are written
383    /// with the writer's *active* component separator and so a literal separator
384    /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
385    /// silently promoted to a component boundary.
386    ///
387    /// Track the `control_ref` — it must be repeated in the matching
388    /// [`end_interchange`](Self::end_interchange) call.
389    ///
390    /// # Example
391    ///
392    /// ```
393    /// use edifact_rs::Writer;
394    /// let mut w = Writer::new(Vec::new());
395    /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
396    /// assert_eq!(
397    ///     w.finish()?,
398    ///     b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
399    /// );
400    /// # Ok::<(), edifact_rs::EdifactError>(())
401    /// ```
402    ///
403    /// # Errors
404    ///
405    /// Returns [`EdifactError`] if writing fails.
406    #[allow(clippy::too_many_arguments)]
407    pub fn begin_interchange(
408        &mut self,
409        syntax_id: &str,
410        syntax_version: &str,
411        sender: &str,
412        recipient: &str,
413        date: &str,
414        time: &str,
415        control_ref: &str,
416    ) -> Result<(), EdifactError> {
417        self.write_composites(
418            "UNB",
419            &[
420                &[syntax_id, syntax_version],
421                &[sender],
422                &[recipient],
423                &[date, time],
424                &[control_ref],
425            ],
426        )
427    }
428
429    /// Write a `UNH` message header and return a [`MessageWriter`] guard.
430    ///
431    /// The guard tracks the per-message segment count automatically.  Call
432    /// [`MessageWriter::finish`] when all message segments have been written — this
433    /// writes the matching `UNT` segment with the correct count.  If `finish` is not
434    /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
435    /// are silently discarded on drop; prefer explicit `finish`).
436    ///
437    /// Generates:
438    /// ```text
439    /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
440    /// ```
441    ///
442    /// # Errors
443    ///
444    /// Returns [`EdifactError`] if writing the `UNH` segment fails.
445    pub fn begin_message<'w>(
446        &'w mut self,
447        message_ref: &str,
448        message_type: &str,
449        version: &str,
450        release: &str,
451        controlling_agency: &str,
452    ) -> Result<MessageWriter<'w, W>, EdifactError> {
453        // Build S009 as an explicit composite.  Formatting it with a literal `:`
454        // and handing it to `write_raw` produced a single collapsed component
455        // whenever the writer used a non-default component separator.
456        self.write_composites(
457            "UNH",
458            &[
459                &[message_ref],
460                &[message_type, version, release, controlling_agency],
461            ],
462        )?;
463        // Capture `segment_count` after writing UNH so `MessageWriter` knows
464        // the absolute count that includes UNH.
465        let unh_count = self.segment_count;
466        Ok(MessageWriter {
467            writer: self,
468            message_ref: message_ref.to_owned(),
469            unh_count,
470            finished: false,
471        })
472    }
473
474    /// Write a `UNZ` interchange trailer segment.
475    ///
476    /// `message_count` is the number of `UNH`/`UNT` message pairs in the
477    /// interchange.  `control_ref` must match the value passed to
478    /// [`begin_interchange`](Self::begin_interchange).
479    ///
480    /// If you used [`begin_message`](Self::begin_message) for every message in the
481    /// interchange, `message_count` equals the number of times you called that
482    /// method.
483    ///
484    /// # Errors
485    ///
486    /// Returns [`EdifactError`] if writing fails.
487    pub fn end_interchange(
488        &mut self,
489        message_count: u32,
490        control_ref: &str,
491    ) -> Result<(), EdifactError> {
492        let msg_count_str = message_count.to_string();
493        self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
494    }
495}
496
497/// RAII guard for a single EDIFACT message within an interchange.
498///
499/// Obtained from [`Writer::begin_message`].  Writes `UNH` on creation and
500/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
501/// is called or the guard is dropped.
502///
503/// Always prefer calling [`finish`](Self::finish) explicitly so that write
504/// errors can be propagated.  The `Drop` impl writes `UNT` as a best-effort
505/// fallback but silently discards I/O errors.
506///
507/// # Example
508///
509/// ```rust,no_run
510/// # use edifact_rs::{Writer, Segment};
511/// # fn example() -> Result<(), edifact_rs::EdifactError> {
512/// let mut writer = Writer::new(Vec::new());
513/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
514/// {
515///     let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
516///     msg.write_raw("BGM", &["220", "PO001", "9"])?;
517///     msg.finish()?;
518/// }
519/// writer.end_interchange(1, "1")?;
520/// # Ok(())
521/// # }
522/// ```
523pub struct MessageWriter<'w, W: Write> {
524    writer: &'w mut Writer<W>,
525    message_ref: String,
526    /// Absolute segment count immediately after `UNH` was written.
527    unh_count: u64,
528    /// Set to `true` once `finish()` has been called to prevent a double-write
529    /// from the `Drop` impl.
530    finished: bool,
531}
532
533impl<W: Write> MessageWriter<'_, W> {
534    /// Write a segment within this message.
535    ///
536    /// Delegates to [`Writer::write_raw`].
537    pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
538        self.writer.write_raw(tag, elements)
539    }
540
541    /// Write a fully-typed segment within this message.
542    ///
543    /// Delegates to [`Writer::write_segment`].
544    pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
545        self.writer.write_segment(seg)
546    }
547
548    /// Compute the per-message segment count and write `UNT`, consuming the guard.
549    ///
550    /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
551    /// and `UNT` itself — matching the EDIFACT standard.
552    ///
553    /// # Errors
554    ///
555    /// Returns [`EdifactError`] if writing the `UNT` segment fails.
556    pub fn finish(mut self) -> Result<(), EdifactError> {
557        self.write_unt()?;
558        self.finished = true;
559        Ok(())
560    }
561
562    fn write_unt(&mut self) -> Result<(), EdifactError> {
563        // Segments since UNH: writer.segment_count - unh_count (content only).
564        // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
565        let count = self.writer.segment_count - self.unh_count + 2;
566        let count_str = count.to_string();
567        self.writer.write_composites(
568            "UNT",
569            &[&[count_str.as_str()], &[self.message_ref.as_str()]],
570        )
571    }
572}
573
574impl<W: Write> Drop for MessageWriter<'_, W> {
575    fn drop(&mut self) {
576        if !self.finished {
577            // Best-effort: write UNT; errors cannot be propagated from drop.
578            let _ = self.write_unt();
579        }
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::model::Element;
587
588    /// A non-default UNA whose delimiters share no byte with the defaults.
589    fn exotic_ssa() -> ServiceStringAdvice {
590        ServiceStringAdvice {
591            component_sep: b'|',
592            element_sep: b'!',
593            decimal_mark: b',',
594            release_char: b'#',
595            repetition_sep: b'*',
596            segment_term: b'~',
597        }
598    }
599
600    #[test]
601    fn unh_composite_uses_the_active_component_separator() {
602        // `begin_message` used to `format!` the S009 composite with a literal
603        // `:`, collapsing it into one component under a custom UNA.
604        let mut buf = Vec::new();
605        {
606            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
607            let msg = w
608                .begin_message("1", "ORDERS", "D", "96A", "UN")
609                .expect("UNH");
610            msg.finish().expect("UNT");
611        }
612        let out = String::from_utf8(buf).unwrap();
613        assert!(
614            out.contains("UNH!1!ORDERS|D|96A|UN~"),
615            "S009 must use `|`, got {out}"
616        );
617    }
618
619    #[test]
620    fn round_trips_through_a_custom_una() {
621        // The library must be able to re-read its own output verbatim.
622        let mut buf = Vec::new();
623        {
624            let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
625            w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
626                .unwrap();
627            let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
628            msg.write_raw("BGM", &["220"]).unwrap();
629            msg.finish().unwrap();
630            w.end_interchange(1, "IC1").unwrap();
631        }
632        let segs: Vec<_> = crate::from_bytes(&buf)
633            .collect::<Result<Vec<_>, _>>()
634            .expect("own output must reparse");
635        let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
636        assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
637        assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
638        crate::validate_envelope(&segs).expect("own output must pass envelope validation");
639    }
640
641    #[test]
642    fn finish_unt_counts_only_the_current_message() {
643        // `finish_unt` used the writer-lifetime segment total, so a preceding
644        // UNB inflated DE 0074 and the interchange failed its own validation.
645        let mut buf = Vec::new();
646        {
647            let mut w = Writer::new(&mut buf);
648            w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
649                .unwrap();
650            w.write_composites("UNH", &[&["1"], &["ORDERS", "D", "96A", "UN"]])
651                .unwrap();
652            w.write_raw("BGM", &["220"]).unwrap();
653            w.finish_unt("1").unwrap();
654        }
655        let out = String::from_utf8(buf).unwrap();
656        // UNH + BGM + UNT == 3
657        assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
658    }
659
660    #[test]
661    fn repetition_separator_is_escaped_when_declared() {
662        let mut buf = Vec::new();
663        {
664            let mut w = Writer::with_una(
665                &mut buf,
666                ServiceStringAdvice {
667                    repetition_sep: b'*',
668                    ..ServiceStringAdvice::default()
669                },
670            )
671            .unwrap();
672            w.write_composites("FTX", &[&["a*b"]]).unwrap();
673        }
674        let out = String::from_utf8(buf).unwrap();
675        assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
676    }
677
678    #[test]
679    fn repetition_separator_sentinel_is_not_escaped() {
680        // Space at UNA position 7 means "not used" and must never be escaped.
681        let w = Writer::new(std::io::sink());
682        assert_eq!(w.escape_value("a b"), "a b");
683    }
684
685    #[test]
686    fn write_composites_escapes_a_literal_component_separator() {
687        let mut buf = Vec::new();
688        {
689            let mut w = Writer::new(&mut buf);
690            w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
691                .unwrap();
692        }
693        let segs: Vec<_> = crate::from_bytes(&buf)
694            .collect::<Result<Vec<_>, _>>()
695            .unwrap();
696        // The `:` stays inside the value instead of splitting the element.
697        assert_eq!(
698            segs[0].get_element(1).unwrap().get_component(0),
699            Some("ACME:INC")
700        );
701    }
702
703    #[test]
704    fn write_and_parse_simple_segment() {
705        let segs: Vec<Segment<'static>> = vec![Segment::new(
706            "BGM",
707            vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
708        )];
709        let bytes = crate::segments_to_bytes(&segs).unwrap();
710        let s = std::str::from_utf8(&bytes).unwrap();
711        assert!(s.starts_with("BGM+220+ORDER123'"));
712    }
713
714    #[test]
715    fn release_char_escaped() {
716        let segs: Vec<Segment<'static>> = vec![Segment::new(
717            "FTX",
718            vec![Element::of(&["value+with+delimiters"])],
719        )];
720        let bytes = crate::segments_to_bytes(&segs).unwrap();
721        let s = std::str::from_utf8(&bytes).unwrap();
722        // The `+` in the value must be escaped as `?+`
723        assert!(s.contains("?+"), "escape missing: {s}");
724    }
725
726    #[test]
727    fn round_trip_preserves_values() {
728        let segs: Vec<Segment<'static>> = vec![
729            Segment::new(
730                "UNB",
731                vec![
732                    Element::of(&["UNOA", "1"]),
733                    Element::of(&["SENDER"]),
734                    Element::of(&["RECEIVER"]),
735                ],
736            ),
737            Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
738        ];
739        let bytes = crate::segments_to_bytes(&segs).unwrap();
740        let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
741            .expect("round-trip parse failed");
742        assert_eq!(rt[0].tag, "UNB");
743        assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
744        assert_eq!(rt[1].tag, "UNZ");
745    }
746
747    /// Verify that `Writer::with_una` uses the configured delimiters throughout,
748    /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
749    /// component separators even with a non-default UNA.
750    #[test]
751    fn with_una_non_default_delimiters() {
752        use crate::tokenizer::ServiceStringAdvice;
753
754        // Custom UNA: comp_sep=|  elem_sep=!  esc=?  dec_mark=,  rep_sep=*  seg_term=~
755        let ssa = ServiceStringAdvice {
756            component_sep: b'|',
757            element_sep: b'!',
758            release_char: b'?',
759            decimal_mark: b',',
760            repetition_sep: b'*',
761            segment_term: b'~',
762        };
763
764        let buf = Vec::new();
765        let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
766
767        // write_segment_parts: pre-split; no hard-coded `:` in element strings
768        writer
769            .write_segment_parts(
770                "BGM",
771                &[
772                    vec!["220".to_owned(), "SUB1".to_owned()],
773                    vec!["PO1".to_owned()],
774                ],
775            )
776            .expect("write failed");
777
778        let out = writer.finish().expect("finish failed");
779        let s = std::str::from_utf8(&out).unwrap();
780
781        // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
782        // The writer also emits a UNA header when with_una is used.
783        assert!(s.contains("BGM"), "BGM segment missing: {s}");
784        // Slice after UNA so assertions target segment output, not UNA header bytes.
785        let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
786        assert!(
787            after_una.contains('!'),
788            "missing element sep in segment: {after_una}"
789        );
790        assert!(
791            after_una.contains('|'),
792            "missing component sep in segment: {after_una}"
793        );
794        assert!(
795            after_una.ends_with('~'),
796            "missing segment term in segment: {after_una}"
797        );
798        // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
799        assert!(s.contains(','), "missing decimal mark in UNA: {s}");
800        assert!(!s.contains('+'), "default element sep leaked: {s}");
801        assert!(!s.contains(':'), "default component sep leaked: {s}");
802        // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
803        assert!(
804            !after_una.contains('\''),
805            "default segment term leaked after UNA: {after_una}"
806        );
807    }
808}