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 one repetition separator byte, or refuse when none is declared.
685 ///
686 /// Emitting the space sentinel would produce output that reads back as a
687 /// single occurrence whose value contains a space — corrupt, and quietly so.
688 #[inline]
689 pub(crate) fn write_repetition_sep(&mut self) -> Result<(), EdifactError> {
690 if !self.ssa.is_repetition_active() {
691 return Err(EdifactError::RepetitionSeparatorNotDeclared);
692 }
693 self.inner.write_all(&[self.ssa.repetition_sep])?;
694 Ok(())
695 }
696
697 /// Write the segment terminator and increment the internal segment counter.
698 #[inline]
699 pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
700 let tag = if self.open_segment_is_unh { "UNH" } else { "" };
701 self.open_segment_is_unh = false;
702 self.end_segment(tag)
703 }
704
705 /// Write text, encoding it into the bound repertoire when there is one.
706 ///
707 /// Callers must only pass slices that start and end on a character boundary.
708 /// Every delimiter is single-byte ASCII (enforced by
709 /// [`ServiceStringAdvice::is_valid`]), so splitting a value at a delimiter
710 /// always satisfies that.
711 #[inline]
712 fn write_text(&mut self, text: &str) -> Result<(), EdifactError> {
713 match self.charset {
714 None => self.inner.write_all(text.as_bytes())?,
715 Some(charset) => self.inner.write_all(&charset.encode(text)?)?,
716 }
717 Ok(())
718 }
719
720 /// Write a value, escaping any delimiter characters.
721 pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
722 let release = self.ssa.release_char;
723 let bytes = value.as_bytes();
724 let mut last = 0;
725 let mut pos = 0;
726 while pos < bytes.len() {
727 let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
728 break;
729 };
730 let abs = pos + hit;
731 if abs > last {
732 self.write_text(&value[last..abs])?;
733 }
734 // The escaped byte is a service character, hence ASCII in every
735 // repertoire — it needs no encoding pass.
736 self.inner.write_all(&[release, bytes[abs]])?;
737 last = abs + 1;
738 pos = abs + 1;
739 }
740 self.write_text(&value[last..])
741 }
742
743 // ── Interchange envelope helpers ──────────────────────────────────────────
744
745 /// Write a `UNB` interchange header segment.
746 ///
747 /// Generates:
748 /// ```text
749 /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
750 /// ```
751 ///
752 /// Composite components (S001 syntax identifier/version, S004 date/time) are
753 /// passed separately rather than pre-joined with `:`, so they are written
754 /// with the writer's *active* component separator and so a literal separator
755 /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
756 /// silently promoted to a component boundary.
757 ///
758 /// Track the `control_ref` — it must be repeated in the matching
759 /// [`end_interchange`](Self::end_interchange) call.
760 ///
761 /// # Example
762 ///
763 /// ```
764 /// use edifact_rs::Writer;
765 /// let mut w = Writer::new(Vec::new());
766 /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
767 /// assert_eq!(
768 /// w.finish()?,
769 /// b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
770 /// );
771 /// # Ok::<(), edifact_rs::EdifactError>(())
772 /// ```
773 ///
774 /// # Errors
775 ///
776 /// Returns [`EdifactError`] if writing fails.
777 #[allow(clippy::too_many_arguments)]
778 pub fn begin_interchange(
779 &mut self,
780 syntax_id: &str,
781 syntax_version: &str,
782 sender: &str,
783 recipient: &str,
784 date: &str,
785 time: &str,
786 control_ref: &str,
787 ) -> Result<(), EdifactError> {
788 // A header that names one repertoire while the body is encoded in another
789 // is the exact silent-corruption failure `with_charset` exists to stop, so
790 // a mismatch is refused rather than written.
791 if let Some(charset) = self.charset {
792 if charset.syntax_identifier() != syntax_id {
793 return Err(EdifactError::CharacterRepertoireMismatch {
794 declared: syntax_id.to_owned(),
795 writer: charset.syntax_identifier(),
796 });
797 }
798 }
799 self.write_composites(
800 "UNB",
801 &[
802 &[syntax_id, syntax_version],
803 &[sender],
804 &[recipient],
805 &[date, time],
806 &[control_ref],
807 ],
808 )
809 }
810
811 /// Write a `UNH` message header and return a [`MessageWriter`] guard.
812 ///
813 /// The guard tracks the per-message segment count automatically. Call
814 /// [`MessageWriter::finish`] when all message segments have been written — this
815 /// writes the matching `UNT` segment with the correct count. If `finish` is not
816 /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
817 /// are silently discarded on drop; prefer explicit `finish`).
818 ///
819 /// Generates:
820 /// ```text
821 /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
822 /// ```
823 ///
824 /// # Errors
825 ///
826 /// Returns [`EdifactError`] if writing the `UNH` segment fails.
827 pub fn begin_message<'w>(
828 &'w mut self,
829 message_ref: &str,
830 message_type: &str,
831 version: &str,
832 release: &str,
833 controlling_agency: &str,
834 ) -> Result<MessageWriter<'w, W>, EdifactError> {
835 // Build S009 as an explicit composite. Formatting it with a literal `:`
836 // and handing it to `write_raw` produced a single collapsed component
837 // whenever the writer used a non-default component separator.
838 self.write_composites(
839 "UNH",
840 &[
841 &[message_ref],
842 &[message_type, version, release, controlling_agency],
843 ],
844 )?;
845 // Capture `segment_count` after writing UNH so `MessageWriter` knows
846 // the absolute count that includes UNH.
847 let unh_count = self.segment_count;
848 Ok(MessageWriter {
849 writer: self,
850 message_ref: message_ref.to_owned(),
851 unh_count,
852 finished: false,
853 })
854 }
855
856 /// Write a `UNZ` interchange trailer segment.
857 ///
858 /// `message_count` is the number of `UNH`/`UNT` message pairs in the
859 /// interchange. `control_ref` must match the value passed to
860 /// [`begin_interchange`](Self::begin_interchange).
861 ///
862 /// If you used [`begin_message`](Self::begin_message) for every message in the
863 /// interchange, `message_count` equals the number of times you called that
864 /// method.
865 ///
866 /// # Errors
867 ///
868 /// Returns [`EdifactError`] if writing fails.
869 pub fn end_interchange(
870 &mut self,
871 message_count: u32,
872 control_ref: &str,
873 ) -> Result<(), EdifactError> {
874 let msg_count_str = message_count.to_string();
875 self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
876 }
877}
878
879/// RAII guard for a single EDIFACT message within an interchange.
880///
881/// Obtained from [`Writer::begin_message`]. Writes `UNH` on creation and
882/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
883/// is called or the guard is dropped.
884///
885/// Always prefer calling [`finish`](Self::finish) explicitly so that write
886/// errors can be propagated. The `Drop` impl writes `UNT` as a best-effort
887/// fallback but silently discards I/O errors.
888///
889/// # Example
890///
891/// ```rust,no_run
892/// # use edifact_rs::{Writer, Segment};
893/// # fn example() -> Result<(), edifact_rs::EdifactError> {
894/// let mut writer = Writer::new(Vec::new());
895/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
896/// {
897/// let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
898/// msg.write_raw("BGM", &["220", "PO001", "9"])?;
899/// msg.finish()?;
900/// }
901/// writer.end_interchange(1, "1")?;
902/// # Ok(())
903/// # }
904/// ```
905pub struct MessageWriter<'w, W: Write> {
906 writer: &'w mut Writer<W>,
907 message_ref: String,
908 /// Absolute segment count immediately after `UNH` was written.
909 unh_count: u64,
910 /// Set to `true` once `finish()` has been called to prevent a double-write
911 /// from the `Drop` impl.
912 finished: bool,
913}
914
915impl<W: Write> MessageWriter<'_, W> {
916 /// Write a segment within this message.
917 ///
918 /// Delegates to [`Writer::write_raw`].
919 pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
920 self.writer.write_raw(tag, elements)
921 }
922
923 /// Write a segment mixing simple and composite data elements within this message.
924 ///
925 /// Delegates to [`Writer::write_elements`] — the general form, and the one
926 /// to reach for when a segment is not uniformly simple or uniformly
927 /// composite.
928 ///
929 /// # Errors
930 ///
931 /// Returns [`EdifactError`] if the underlying writer fails.
932 pub fn write_elements(
933 &mut self,
934 tag: &str,
935 elements: &[DataElement<'_>],
936 ) -> Result<(), EdifactError> {
937 self.writer.write_elements(tag, elements)
938 }
939
940 /// Write a segment from borrowed element/component slices within this message.
941 ///
942 /// Delegates to [`Writer::write_composites`].
943 ///
944 /// # Errors
945 ///
946 /// Returns [`EdifactError`] if the underlying writer fails.
947 pub fn write_composites(
948 &mut self,
949 tag: &str,
950 elements: &[&[&str]],
951 ) -> Result<(), EdifactError> {
952 self.writer.write_composites(tag, elements)
953 }
954
955 /// Write a segment from pre-split, owned element/component data within this message.
956 ///
957 /// Delegates to [`Writer::write_segment_parts`].
958 ///
959 /// # Errors
960 ///
961 /// Returns [`EdifactError`] if the underlying writer fails.
962 pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
963 where
964 E: AsRef<[String]>,
965 {
966 self.writer.write_segment_parts(tag, elements)
967 }
968
969 /// Write a fully-typed segment within this message.
970 ///
971 /// Delegates to [`Writer::write_segment`].
972 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
973 self.writer.write_segment(seg)
974 }
975
976 /// Compute the per-message segment count and write `UNT`, consuming the guard.
977 ///
978 /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
979 /// and `UNT` itself — matching the EDIFACT standard.
980 ///
981 /// # Errors
982 ///
983 /// Returns [`EdifactError`] if writing the `UNT` segment fails.
984 pub fn finish(mut self) -> Result<(), EdifactError> {
985 self.write_unt()?;
986 self.finished = true;
987 Ok(())
988 }
989
990 fn write_unt(&mut self) -> Result<(), EdifactError> {
991 // Segments since UNH: writer.segment_count - unh_count (content only).
992 // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
993 let count = self.writer.segment_count - self.unh_count + 2;
994 let count_str = count.to_string();
995 self.writer.write_composites(
996 "UNT",
997 &[&[count_str.as_str()], &[self.message_ref.as_str()]],
998 )
999 }
1000}
1001
1002impl<W: Write> Drop for MessageWriter<'_, W> {
1003 fn drop(&mut self) {
1004 if !self.finished {
1005 // Best-effort: write UNT; errors cannot be propagated from drop.
1006 let _ = self.write_unt();
1007 }
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014 use crate::model::Element;
1015
1016 /// A non-default UNA whose delimiters share no byte with the defaults.
1017 fn exotic_ssa() -> ServiceStringAdvice {
1018 ServiceStringAdvice {
1019 component_sep: b'|',
1020 element_sep: b'!',
1021 decimal_mark: b',',
1022 release_char: b'#',
1023 repetition_sep: b'*',
1024 segment_term: b'~',
1025 }
1026 }
1027
1028 #[test]
1029 fn unh_composite_uses_the_active_component_separator() {
1030 // `begin_message` used to `format!` the S009 composite with a literal
1031 // `:`, collapsing it into one component under a custom UNA.
1032 let mut buf = Vec::new();
1033 {
1034 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1035 let msg = w
1036 .begin_message("1", "ORDERS", "D", "96A", "UN")
1037 .expect("UNH");
1038 msg.finish().expect("UNT");
1039 }
1040 let out = String::from_utf8(buf).unwrap();
1041 assert!(
1042 out.contains("UNH!1!ORDERS|D|96A|UN~"),
1043 "S009 must use `|`, got {out}"
1044 );
1045 }
1046
1047 #[test]
1048 fn round_trips_through_a_custom_una() {
1049 // The library must be able to re-read its own output verbatim.
1050 let mut buf = Vec::new();
1051 {
1052 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1053 w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
1054 .unwrap();
1055 let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1056 msg.write_raw("BGM", &["220"]).unwrap();
1057 msg.finish().unwrap();
1058 w.end_interchange(1, "IC1").unwrap();
1059 }
1060 let segs: Vec<_> = crate::from_bytes(&buf)
1061 .collect::<Result<Vec<_>, _>>()
1062 .expect("own output must reparse");
1063 let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
1064 assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
1065 assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
1066 crate::validate_envelope(&segs).expect("own output must pass envelope validation");
1067 }
1068
1069 #[test]
1070 fn finish_unt_counts_only_the_current_message() {
1071 // `finish_unt` used the writer-lifetime segment total, so a preceding
1072 // UNB inflated DE 0074 and the interchange failed its own validation.
1073 let mut buf = Vec::new();
1074 {
1075 let mut w = Writer::new(&mut buf);
1076 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1077 .unwrap();
1078 w.write_composites("UNH", &[&["1"], &["ORDERS", "D", "96A", "UN"]])
1079 .unwrap();
1080 w.write_raw("BGM", &["220"]).unwrap();
1081 w.finish_unt("1").unwrap();
1082 }
1083 let out = String::from_utf8(buf).unwrap();
1084 // UNH + BGM + UNT == 3
1085 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1086 }
1087
1088 #[test]
1089 fn repetition_separator_is_escaped_when_declared() {
1090 let mut buf = Vec::new();
1091 {
1092 let mut w = Writer::with_una(
1093 &mut buf,
1094 ServiceStringAdvice {
1095 repetition_sep: b'*',
1096 ..ServiceStringAdvice::default()
1097 },
1098 )
1099 .unwrap();
1100 w.write_composites("FTX", &[&["a*b"]]).unwrap();
1101 }
1102 let out = String::from_utf8(buf).unwrap();
1103 assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
1104 }
1105
1106 #[test]
1107 fn repetition_separator_sentinel_is_not_escaped() {
1108 // Space at UNA position 7 means "not used" and must never be escaped.
1109 let w = Writer::new(std::io::sink());
1110 assert_eq!(w.escape_value("a b"), "a b");
1111 }
1112
1113 #[test]
1114 fn write_composites_escapes_a_literal_component_separator() {
1115 let mut buf = Vec::new();
1116 {
1117 let mut w = Writer::new(&mut buf);
1118 w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
1119 .unwrap();
1120 }
1121 let segs: Vec<_> = crate::from_bytes(&buf)
1122 .collect::<Result<Vec<_>, _>>()
1123 .unwrap();
1124 // The `:` stays inside the value instead of splitting the element.
1125 assert_eq!(
1126 segs[0].get_element(1).unwrap().get_component(0),
1127 Some("ACME:INC")
1128 );
1129 }
1130
1131 #[test]
1132 fn write_elements_mixes_simple_and_composite() {
1133 let mut buf = Vec::new();
1134 {
1135 let mut w = Writer::new(&mut buf);
1136 w.write_elements(
1137 "NAD",
1138 &[
1139 DataElement::Simple("MS"),
1140 DataElement::Composite(&["9900112233445", "", "293"]),
1141 ],
1142 )
1143 .unwrap();
1144 }
1145 assert_eq!(buf, b"NAD+MS+9900112233445::293'");
1146 }
1147
1148 #[test]
1149 fn elements_macro_matches_the_explicit_form() {
1150 let mut macro_buf = Vec::new();
1151 {
1152 let mut w = Writer::new(&mut macro_buf);
1153 w.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1154 .unwrap();
1155 w.write_elements("DTM", elements![["137", "20260101", "102"]])
1156 .unwrap();
1157 }
1158 let mut explicit_buf = Vec::new();
1159 {
1160 let mut w = Writer::new(&mut explicit_buf);
1161 w.write_elements(
1162 "NAD",
1163 &[
1164 DataElement::Simple("MS"),
1165 DataElement::Composite(&["ACME", "", "9"]),
1166 ],
1167 )
1168 .unwrap();
1169 w.write_elements(
1170 "DTM",
1171 &[DataElement::Composite(&["137", "20260101", "102"])],
1172 )
1173 .unwrap();
1174 }
1175 assert_eq!(macro_buf, explicit_buf);
1176 assert_eq!(macro_buf, b"NAD+MS+ACME::9'DTM+137:20260101:102'");
1177 }
1178
1179 #[test]
1180 fn elements_macro_accepts_arbitrary_expressions() {
1181 // Builders emit runtime values, not literals. A `tt`-based macro only
1182 // matched single-token entries, so `qualifier.as_str()` failed to parse
1183 // — which is precisely the shape this macro exists for.
1184 let qualifier = String::from("MS");
1185 let gln = "9900112233445";
1186 let dtm: Vec<&str> = vec!["137", "20260101", "102"];
1187
1188 let mut buf = Vec::new();
1189 {
1190 let mut w = Writer::new(&mut buf);
1191 w.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])
1192 .unwrap();
1193 w.write_elements("DTM", elements![dtm]).unwrap();
1194 w.write_elements("FTX", elements![qualifier]).unwrap();
1195 w.write_elements("UNS", elements![]).unwrap();
1196 }
1197 assert_eq!(
1198 String::from_utf8(buf).unwrap(),
1199 "NAD+MS+9900112233445::293'DTM+137:20260101:102'FTX+MS'UNS'"
1200 );
1201 }
1202
1203 #[test]
1204 fn write_elements_escapes_a_literal_component_separator() {
1205 // The `:` stays inside the value instead of splitting the element —
1206 // the failure mode of pre-joining components into one string.
1207 let mut buf = Vec::new();
1208 {
1209 let mut w = Writer::new(&mut buf);
1210 w.write_elements(
1211 "NAD",
1212 &[DataElement::Simple("MS"), DataElement::Simple("ACME:INC")],
1213 )
1214 .unwrap();
1215 }
1216 let segs: Vec<_> = crate::from_bytes(&buf)
1217 .collect::<Result<Vec<_>, _>>()
1218 .unwrap();
1219 assert_eq!(
1220 segs[0].get_element(1).unwrap().get_component(0),
1221 Some("ACME:INC")
1222 );
1223 }
1224
1225 #[test]
1226 fn write_elements_uses_the_active_component_separator() {
1227 let mut buf = Vec::new();
1228 {
1229 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1230 w.write_elements("DTM", elements![["137", "20260101", "102"]])
1231 .unwrap();
1232 }
1233 let out = String::from_utf8(buf).unwrap();
1234 assert!(
1235 out.ends_with("DTM!137|20260101|102~"),
1236 "expected custom delimiters, got {out}"
1237 );
1238 }
1239
1240 #[test]
1241 fn message_writer_counts_write_elements_segments() {
1242 // `MessageWriter` had no mixed-emit delegate, so callers dropped to the
1243 // raw writer and their segments escaped the UNT DE 0074 count.
1244 let mut buf = Vec::new();
1245 {
1246 let mut w = Writer::new(&mut buf);
1247 let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1248 msg.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1249 .unwrap();
1250 msg.write_composites("DTM", &[&["137", "20260101", "102"]])
1251 .unwrap();
1252 msg.finish().unwrap();
1253 }
1254 let out = String::from_utf8(buf).unwrap();
1255 // UNH + NAD + DTM + UNT == 4
1256 assert!(out.contains("UNT+4+1'"), "expected UNT+4, got {out}");
1257 }
1258
1259 #[test]
1260 fn write_segment_records_unh_for_the_unt_count() {
1261 // `write_segment` and `write_segment_parts` did not record the UNH
1262 // marker, so `finish_unt` fell back to the writer-lifetime total and
1263 // DE 0074 came out inflated by every preceding interchange segment.
1264 let mut buf = Vec::new();
1265 {
1266 let mut w = Writer::new(&mut buf);
1267 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1268 .unwrap();
1269 w.write_segment(&Segment::new(
1270 "UNH",
1271 vec![
1272 Element::of(&["1"]),
1273 Element::of(&["ORDERS", "D", "96A", "UN"]),
1274 ],
1275 ))
1276 .unwrap();
1277 w.write_segment(&Segment::new("BGM", vec![Element::of(&["220"])]))
1278 .unwrap();
1279 w.finish_unt("1").unwrap();
1280 }
1281 let out = String::from_utf8(buf).unwrap();
1282 // UNH + BGM + UNT == 3, not 4 (which would count the UNB).
1283 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1284 }
1285
1286 #[test]
1287 fn write_segment_parts_records_unh_for_the_unt_count() {
1288 let mut buf = Vec::new();
1289 {
1290 let mut w = Writer::new(&mut buf);
1291 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1292 .unwrap();
1293 w.write_segment_parts(
1294 "UNH",
1295 &[
1296 vec!["1".to_owned()],
1297 vec![
1298 "ORDERS".to_owned(),
1299 "D".to_owned(),
1300 "96A".to_owned(),
1301 "UN".to_owned(),
1302 ],
1303 ],
1304 )
1305 .unwrap();
1306 w.write_raw("BGM", &["220"]).unwrap();
1307 w.finish_unt("1").unwrap();
1308 }
1309 let out = String::from_utf8(buf).unwrap();
1310 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1311 }
1312
1313 #[test]
1314 fn escape_value_handles_multi_byte_text() {
1315 // The old implementation assembled a `Vec<u8>` and re-validated it with
1316 // an `expect`. Escaping around non-ASCII text is the case that made
1317 // that conversion look fallible in the first place.
1318 let w = Writer::new(std::io::sink());
1319 assert_eq!(w.escape_value("Grüße+Köln"), "Grüße?+Köln");
1320 assert_eq!(w.escape_value("Grüße"), "Grüße");
1321 assert!(matches!(w.escape_value("plain"), Cow::Borrowed("plain")));
1322 }
1323
1324 #[test]
1325 fn write_and_parse_simple_segment() {
1326 let segs: Vec<Segment<'static>> = vec![Segment::new(
1327 "BGM",
1328 vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
1329 )];
1330 let bytes = crate::segments_to_bytes(&segs).unwrap();
1331 let s = std::str::from_utf8(&bytes).unwrap();
1332 assert!(s.starts_with("BGM+220+ORDER123'"));
1333 }
1334
1335 #[test]
1336 fn release_char_escaped() {
1337 let segs: Vec<Segment<'static>> = vec![Segment::new(
1338 "FTX",
1339 vec![Element::of(&["value+with+delimiters"])],
1340 )];
1341 let bytes = crate::segments_to_bytes(&segs).unwrap();
1342 let s = std::str::from_utf8(&bytes).unwrap();
1343 // The `+` in the value must be escaped as `?+`
1344 assert!(s.contains("?+"), "escape missing: {s}");
1345 }
1346
1347 #[test]
1348 fn round_trip_preserves_values() {
1349 let segs: Vec<Segment<'static>> = vec![
1350 Segment::new(
1351 "UNB",
1352 vec![
1353 Element::of(&["UNOA", "1"]),
1354 Element::of(&["SENDER"]),
1355 Element::of(&["RECEIVER"]),
1356 ],
1357 ),
1358 Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1359 ];
1360 let bytes = crate::segments_to_bytes(&segs).unwrap();
1361 let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
1362 .expect("round-trip parse failed");
1363 assert_eq!(rt[0].tag, "UNB");
1364 assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
1365 assert_eq!(rt[1].tag, "UNZ");
1366 }
1367
1368 /// Verify that `Writer::with_una` uses the configured delimiters throughout,
1369 /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
1370 /// component separators even with a non-default UNA.
1371 #[test]
1372 fn with_una_non_default_delimiters() {
1373 use crate::tokenizer::ServiceStringAdvice;
1374
1375 // Custom UNA: comp_sep=| elem_sep=! esc=? dec_mark=, rep_sep=* seg_term=~
1376 let ssa = ServiceStringAdvice {
1377 component_sep: b'|',
1378 element_sep: b'!',
1379 release_char: b'?',
1380 decimal_mark: b',',
1381 repetition_sep: b'*',
1382 segment_term: b'~',
1383 };
1384
1385 let buf = Vec::new();
1386 let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
1387
1388 // write_segment_parts: pre-split; no hard-coded `:` in element strings
1389 writer
1390 .write_segment_parts(
1391 "BGM",
1392 &[
1393 vec!["220".to_owned(), "SUB1".to_owned()],
1394 vec!["PO1".to_owned()],
1395 ],
1396 )
1397 .expect("write failed");
1398
1399 let out = writer.finish().expect("finish failed");
1400 let s = std::str::from_utf8(&out).unwrap();
1401
1402 // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
1403 // The writer also emits a UNA header when with_una is used.
1404 assert!(s.contains("BGM"), "BGM segment missing: {s}");
1405 // Slice after UNA so assertions target segment output, not UNA header bytes.
1406 let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
1407 assert!(
1408 after_una.contains('!'),
1409 "missing element sep in segment: {after_una}"
1410 );
1411 assert!(
1412 after_una.contains('|'),
1413 "missing component sep in segment: {after_una}"
1414 );
1415 assert!(
1416 after_una.ends_with('~'),
1417 "missing segment term in segment: {after_una}"
1418 );
1419 // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
1420 assert!(s.contains(','), "missing decimal mark in UNA: {s}");
1421 assert!(!s.contains('+'), "default element sep leaked: {s}");
1422 assert!(!s.contains(':'), "default component sep leaked: {s}");
1423 // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
1424 assert!(
1425 !after_una.contains('\''),
1426 "default segment term leaked after UNA: {after_una}"
1427 );
1428 }
1429}