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