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