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