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/// # What it will not write
206///
207/// Everything the writer emits reparses. Two cases are refused before a byte
208/// reaches the sink, so a rejected segment leaves nothing half-written:
209///
210/// - A segment tag that is not three ASCII uppercase letters
211/// ([`EdifactError::InvalidSegmentTag`]). A tag is written verbatim — EDIFACT
212/// has no way to escape one — so `bgm`, `BGMX`, or `B+M` would produce bytes
213/// that do not read back as the segment they came from.
214/// - A repeating data element when no repetition separator is declared
215/// ([`EdifactError::RepetitionSeparatorNotDeclared`]).
216///
217/// Delimiters *inside a value* are not a problem: those are release-escaped.
218///
219/// # Wrap unbuffered sinks
220///
221/// `Writer` issues a separate write for each tag, delimiter, and value chunk, so
222/// a segment costs roughly one write per component. Against an in-memory
223/// `Vec<u8>` that is free, but against a [`File`][std::fs::File] or a socket each
224/// one is a syscall.
225///
226/// The writer deliberately does **not** buffer internally: an internal buffer
227/// would silently discard everything not yet flushed if the writer were dropped
228/// without [`finish`][Self::finish]. Wrap the sink instead, which makes the
229/// buffering visible and keeps the flush contract in one place:
230///
231/// ```rust
232/// use std::io::BufWriter;
233/// use edifact_rs::Writer;
234///
235/// let sink = Vec::new(); // stands in for a File or TcpStream
236/// let mut writer = Writer::new(BufWriter::new(sink));
237/// writer.write_simple("BGM", &["220"])?;
238/// // `finish` flushes the `Writer` and hands the `BufWriter` back.
239/// let buffered = writer.finish()?;
240/// assert_eq!(buffered.into_inner().unwrap(), b"BGM+220'".to_vec());
241/// # Ok::<(), edifact_rs::EdifactError>(())
242/// ```
243pub struct Writer<W: Write> {
244 inner: W,
245 ssa: ServiceStringAdvice,
246 /// Running count of segments written. `u64` to prevent silent overflow on
247 /// pathological inputs (a `u32` would wrap after ~4 billion segments).
248 segment_count: u64,
249 /// `segment_count` as of the most recent `UNH`, used by [`Writer::finish_unt`]
250 /// to derive a per-message DE 0074 rather than a writer-lifetime total.
251 message_start_count: u64,
252 /// Whether the segment currently being written incrementally (via the
253 /// event-emitter path) is a `UNH`. The whole-segment methods pass the tag
254 /// to `end_segment` directly; the emitter only sees it at `StartSegment`.
255 open_segment_is_unh: bool,
256 /// Repertoire every value is encoded into, when the writer is bound to one.
257 ///
258 /// `None` emits UTF-8 unchecked, which is correct for `UNOY` and for any
259 /// payload that happens to be ASCII.
260 charset: Option<Charset>,
261}
262
263/// Return the offset of the first byte in `hay` that must be release-escaped.
264///
265/// The escape set is the four splitting delimiters plus the repetition separator
266/// when the active UNA declares one. A space at UNA position 7 is the
267/// conventional "not used" sentinel and is never escaped.
268#[inline]
269fn find_escape(ssa: &ServiceStringAdvice, hay: &[u8]) -> Option<usize> {
270 let first = memchr::memchr3(ssa.element_sep, ssa.component_sep, ssa.release_char, hay);
271 let second = if ssa.repetition_sep == b' ' {
272 memchr::memchr(ssa.segment_term, hay)
273 } else {
274 memchr::memchr2(ssa.segment_term, ssa.repetition_sep, hay)
275 };
276 match (first, second) {
277 (None, None) => None,
278 (Some(a), None) => Some(a),
279 (None, Some(b)) => Some(b),
280 (Some(a), Some(b)) => Some(a.min(b)),
281 }
282}
283
284impl<W: Write> Writer<W> {
285 /// Write the segment tag, refusing one the parser would not read back.
286 ///
287 /// A tag is emitted verbatim — EDIFACT has no way to escape it — so a
288 /// lowercase, mis-sized, or delimiter-bearing tag produces bytes that do not
289 /// reparse as the segment they came from. Checked *before* the first byte
290 /// reaches the sink, so a refused segment leaves nothing behind it.
291 ///
292 /// Applies the same predicate as the parser, so the two cannot drift.
293 #[inline]
294 fn write_tag(&mut self, tag: &str) -> Result<(), EdifactError> {
295 if !crate::tokenizer::is_valid_segment_tag(tag) {
296 return Err(EdifactError::InvalidSegmentTag(tag.to_owned()));
297 }
298 self.inner.write_all(tag.as_bytes())?;
299 Ok(())
300 }
301
302 /// Create a new writer with default EDIFACT delimiters.
303 pub fn new(inner: W) -> Self {
304 Self {
305 inner,
306 ssa: ServiceStringAdvice::default(),
307 segment_count: 0,
308 message_start_count: 0,
309 open_segment_is_unh: false,
310 charset: None,
311 }
312 }
313
314 /// Bind this writer to a character repertoire.
315 ///
316 /// Every value is then encoded into `charset` rather than emitted as UTF-8,
317 /// and a character the repertoire cannot carry is rejected with
318 /// [`EdifactError::CharacterNotInRepertoire`] instead of being written as
319 /// bytes the receiver decodes as something else.
320 ///
321 /// This is the write-side counterpart of
322 /// [`decode_interchange`][crate::decode_interchange]: a `UNOC` interchange
323 /// must go out as ISO 8859-1, not UTF-8, or `ü` arrives as two mojibake
324 /// characters.
325 ///
326 /// # Example
327 ///
328 /// ```
329 /// use edifact_rs::{Charset, Writer};
330 ///
331 /// let mut writer = Writer::new(Vec::new()).with_charset(Charset::UnoC);
332 /// writer.write_composites("NAD", &[&["BY"], &["Müller"]])?;
333 /// // `ü` goes out as the single Latin-1 byte 0xFC.
334 /// assert_eq!(writer.finish()?, b"NAD+BY+M\xFCller'".to_vec());
335 /// # Ok::<(), edifact_rs::EdifactError>(())
336 /// ```
337 ///
338 /// A value outside the repertoire is refused:
339 ///
340 /// ```
341 /// use edifact_rs::{Charset, EdifactError, Writer};
342 ///
343 /// let mut writer = Writer::new(Vec::new()).with_charset(Charset::UnoA);
344 /// // Level A is upper-case only.
345 /// let err = writer.write_composites("NAD", &[&["BY"], &["Müller"]]).unwrap_err();
346 /// assert!(matches!(err, EdifactError::CharacterNotInRepertoire { .. }));
347 /// ```
348 #[must_use]
349 pub fn with_charset(mut self, charset: Charset) -> Self {
350 self.charset = Some(charset);
351 self
352 }
353
354 /// The repertoire this writer encodes into, if it is bound to one.
355 #[must_use]
356 pub fn charset(&self) -> Option<Charset> {
357 self.charset
358 }
359
360 /// Create a writer that uses `ssa`'s delimiters **without** emitting a `UNA`.
361 ///
362 /// For replying on an inbound interchange's delimiters, or round-tripping a
363 /// syntax-version-4 interchange that has repeating elements but no `UNA`:
364 /// the writer needs the repetition separator, and
365 /// [`with_una`][Self::with_una] would add a header the original lacked.
366 ///
367 /// # Errors
368 ///
369 /// [`EdifactError::InvalidUna`] when the service characters are not mutually
370 /// distinct printable non-alphanumeric ASCII — see
371 /// [`ServiceStringAdvice::is_valid`].
372 ///
373 /// # Example
374 ///
375 /// ```
376 /// use edifact_rs::{ServiceStringAdvice, Writer, from_bytes};
377 ///
378 /// // Version 4: `*` separates RFF's two occurrences, with no UNA to say so.
379 /// let input = b"UNB+UNOC:4+S+R+260101:0900+I'RFF+ON:1*ON:2'UNZ+0+I'";
380 /// let segments: Vec<_> = from_bytes(input).collect::<Result<Vec<_>, _>>()?;
381 ///
382 /// let ssa = ServiceStringAdvice::for_syntax_version(Some(4));
383 /// let mut writer = Writer::with_service_string_advice(Vec::new(), ssa)?;
384 /// for segment in &segments {
385 /// writer.write_segment(segment)?;
386 /// }
387 /// // Byte-for-byte the input, with no UNA invented.
388 /// assert_eq!(writer.finish()?, input.to_vec());
389 /// # Ok::<(), edifact_rs::EdifactError>(())
390 /// ```
391 pub fn with_service_string_advice(
392 inner: W,
393 ssa: ServiceStringAdvice,
394 ) -> Result<Self, EdifactError> {
395 if !ssa.is_valid() {
396 return Err(EdifactError::InvalidUna);
397 }
398 Ok(Self {
399 inner,
400 ssa,
401 segment_count: 0,
402 message_start_count: 0,
403 open_segment_is_unh: false,
404 charset: None,
405 })
406 }
407
408 /// Create a writer with custom delimiters and write a `UNA` segment first.
409 ///
410 /// [`with_service_string_advice`][Self::with_service_string_advice] is the
411 /// same thing without the header.
412 ///
413 /// # Errors
414 ///
415 /// As [`with_service_string_advice`][Self::with_service_string_advice], plus
416 /// any write failure.
417 pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
418 // All five active service characters must be mutually distinct, non-whitespace,
419 // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
420 if !ssa.is_valid() {
421 return Err(EdifactError::InvalidUna);
422 }
423 // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
424 let una = [
425 b'U',
426 b'N',
427 b'A',
428 ssa.component_sep,
429 ssa.element_sep,
430 ssa.decimal_mark,
431 ssa.release_char,
432 ssa.repetition_sep,
433 ssa.segment_term,
434 ];
435 inner.write_all(&una)?;
436 Ok(Self {
437 inner,
438 ssa,
439 segment_count: 0,
440 message_start_count: 0,
441 open_segment_is_unh: false,
442 charset: None,
443 })
444 }
445
446 /// Record the end of a segment: terminator, count, and `UNH` bookkeeping.
447 ///
448 /// Every emit path funnels through here, so
449 /// [`finish_unt`][Self::finish_unt] derives DE 0074 from the current
450 /// message rather than the writer-lifetime total.
451 #[inline]
452 fn end_segment(&mut self, tag: &str) -> Result<(), EdifactError> {
453 self.inner.write_all(&[self.ssa.segment_term])?;
454 if tag == "UNH" {
455 self.message_start_count = self.segment_count;
456 }
457 self.segment_count += 1;
458 Ok(())
459 }
460
461 /// Write a single segment, including any ISO 9735-4 repetitions.
462 ///
463 /// # Errors
464 ///
465 /// Returns [`EdifactError::RepetitionSeparatorNotDeclared`] when the segment
466 /// carries a repeating data element but the active service string advice
467 /// declares no repetition separator. The check runs **before** any byte is
468 /// written, so a rejected segment leaves nothing behind in the sink — a
469 /// half-written `RFF+` would otherwise corrupt the interchange for every
470 /// caller that recovers from the error and carries on.
471 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
472 // The sentinel at UNA position 7 is a space. Emitting it as a separator
473 // would produce output that reads back as a single occurrence whose
474 // value contains a space — corrupt, and quietly so. Refusing is the
475 // only honest option, and refusing before the first write is the only
476 // one that keeps the sink consistent.
477 if !self.ssa.is_repetition_active() && seg.elements.iter().any(|e| !e.repeats.is_empty()) {
478 return Err(EdifactError::RepetitionSeparatorNotDeclared);
479 }
480
481 self.write_tag(seg.tag())?;
482
483 for element in &seg.elements {
484 self.inner.write_all(&[self.ssa.element_sep])?;
485 for (repetition, components) in element.repetitions().enumerate() {
486 if repetition > 0 {
487 self.inner.write_all(&[self.ssa.repetition_sep])?;
488 }
489 for (i, (component, _)) in components.iter().enumerate() {
490 if i > 0 {
491 self.inner.write_all(&[self.ssa.component_sep])?;
492 }
493 self.write_escaped(component)?;
494 }
495 }
496 }
497
498 self.end_segment(seg.tag())
499 }
500
501 /// Write a segment whose data elements are all **simple** — one value each.
502 ///
503 /// The shorthand for the commonest segment shape. Each string is one whole
504 /// data element: a component separator inside a value is escaped as data,
505 /// not promoted to a component boundary, so the output is correct whatever
506 /// delimiters the writer uses.
507 ///
508 /// Reach for [`write_composites`][Self::write_composites] when the elements
509 /// have components, or [`write_elements`][Self::write_elements] when the
510 /// segment mixes the two shapes.
511 ///
512 /// # Example
513 ///
514 /// ```
515 /// use edifact_rs::Writer;
516 ///
517 /// let mut w = Writer::new(Vec::new());
518 /// w.write_simple("BGM", &["220", "PO-4711", "9"])?;
519 /// // A literal `:` stays inside the value it belongs to.
520 /// w.write_simple("FTX", &["AAA", "ACME:INC"])?;
521 /// assert_eq!(w.finish()?, b"BGM+220+PO-4711+9'FTX+AAA+ACME?:INC'".to_vec());
522 /// # Ok::<(), edifact_rs::EdifactError>(())
523 /// ```
524 ///
525 /// # Errors
526 ///
527 /// Returns [`EdifactError`] if the underlying writer fails, or if a value
528 /// cannot be encoded in this writer's [`Charset`].
529 pub fn write_simple<S: AsRef<str>>(
530 &mut self,
531 tag: &str,
532 elements: &[S],
533 ) -> Result<(), EdifactError> {
534 self.write_tag(tag)?;
535 for element in elements {
536 self.inner.write_all(&[self.ssa.element_sep])?;
537 self.write_escaped(element.as_ref())?;
538 }
539 self.end_segment(tag)
540 }
541
542 /// Write a segment whose data elements are all **composite** — a list of
543 /// components each.
544 ///
545 /// Component boundaries are given explicitly rather than inferred by
546 /// splitting, so a value containing the active component separator is
547 /// escaped instead of being silently reinterpreted as a boundary.
548 ///
549 /// The bounds accept borrowed and owned data alike — `&[&[&str]]`,
550 /// `&[Vec<String>]`, `&[[String; 3]]` — so runtime-built segments need no
551 /// conversion. A one-component element is a simple data element, which is
552 /// what makes this the general all-elements form;
553 /// [`write_elements`][Self::write_elements] is the shorthand for the mixed
554 /// shape and [`write_simple`][Self::write_simple] for the all-simple one.
555 ///
556 /// # Example
557 ///
558 /// ```
559 /// use edifact_rs::Writer;
560 ///
561 /// let mut w = Writer::new(Vec::new());
562 /// // Borrowed literals …
563 /// w.write_composites("NAD", &[&["MS"][..], &["ACME:INC"][..]])?;
564 /// // … and owned, runtime-built data, through the same call.
565 /// let dtm = vec![vec!["137".to_string(), "20260101".to_string()]];
566 /// w.write_composites("DTM", &dtm)?;
567 /// assert_eq!(w.finish()?, b"NAD+MS+ACME?:INC'DTM+137:20260101'".to_vec());
568 /// # Ok::<(), edifact_rs::EdifactError>(())
569 /// ```
570 ///
571 /// # Errors
572 ///
573 /// Returns [`EdifactError`] if the underlying writer fails, or if a value
574 /// cannot be encoded in this writer's [`Charset`].
575 pub fn write_composites<E, S>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
576 where
577 E: AsRef<[S]>,
578 S: AsRef<str>,
579 {
580 self.write_tag(tag)?;
581 for element in elements {
582 self.inner.write_all(&[self.ssa.element_sep])?;
583 for (i, comp) in element.as_ref().iter().enumerate() {
584 if i > 0 {
585 self.inner.write_all(&[self.ssa.component_sep])?;
586 }
587 self.write_escaped(comp.as_ref())?;
588 }
589 }
590 self.end_segment(tag)
591 }
592
593 /// Write a segment whose data elements mix simple and composite shapes.
594 ///
595 /// This is the general form of segment emission and the one that matches
596 /// how EDIFACT segments are actually specified: `NAD` takes a simple
597 /// qualifier followed by a composite party identification, `DTM` takes a
598 /// single composite. [`write_simple`][Self::write_simple] and
599 /// [`write_composites`][Self::write_composites] are the uniform special
600 /// cases.
601 ///
602 /// Component boundaries are explicit, so a value containing the active
603 /// component separator is escaped rather than silently promoted to a
604 /// boundary. Nothing is allocated.
605 ///
606 /// # Example
607 ///
608 /// ```rust
609 /// use edifact_rs::{DataElement, Writer, elements};
610 ///
611 /// let mut w = Writer::new(Vec::new());
612 /// // Explicit form …
613 /// w.write_elements(
614 /// "NAD",
615 /// &[DataElement::Simple("MS"), DataElement::Composite(&["ACME:INC", "", "9"])],
616 /// )?;
617 /// // … or the `elements!` shorthand.
618 /// w.write_elements("DTM", elements![["137", "20260101", "102"]])?;
619 /// assert_eq!(
620 /// w.finish()?,
621 /// b"NAD+MS+ACME?:INC::9'DTM+137:20260101:102'".to_vec(),
622 /// );
623 /// # Ok::<(), edifact_rs::EdifactError>(())
624 /// ```
625 ///
626 /// # Errors
627 ///
628 /// Returns [`EdifactError`] if the underlying writer fails.
629 pub fn write_elements(
630 &mut self,
631 tag: &str,
632 elements: &[DataElement<'_>],
633 ) -> Result<(), EdifactError> {
634 self.write_tag(tag)?;
635 for element in elements {
636 self.inner.write_all(&[self.ssa.element_sep])?;
637 for (i, comp) in element.components().iter().enumerate() {
638 if i > 0 {
639 self.inner.write_all(&[self.ssa.component_sep])?;
640 }
641 self.write_escaped(comp)?;
642 }
643 }
644 self.end_segment(tag)
645 }
646
647 /// Flush and return the underlying writer.
648 pub fn finish(mut self) -> Result<W, EdifactError> {
649 self.inner.flush()?;
650 Ok(self.inner)
651 }
652
653 /// Write the `UNT` segment and return the inner writer.
654 ///
655 /// The count written into `UNT` DE 0074 covers the current message only:
656 /// `UNH`, every segment written since it, and `UNT` itself. Segments written
657 /// before the message's `UNH` — an interchange-level `UNB`, or a preceding
658 /// message — are excluded, as EDIFACT requires.
659 ///
660 /// If no `UNH` has been written, the count falls back to every segment
661 /// written so far plus one.
662 ///
663 /// # Errors
664 ///
665 /// Returns an error if writing fails. Do **not** call [`write_simple`][Self::write_simple] or
666 /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
667 pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
668 // DE 0074 counts UNH + content + UNT. `message_start_count` is the
669 // absolute segment count immediately after UNH, so content is
670 // `segment_count - message_start_count` and the total adds UNH and UNT.
671 let count = self.segment_count - self.message_start_count + 1;
672 let count_str = count.to_string();
673 self.write_composites("UNT", &[&[count_str.as_str()], &[message_ref]])?;
674 self.finish()
675 }
676
677 /// Returns the total number of segments written so far.
678 pub fn segment_count(&self) -> u64 {
679 self.segment_count
680 }
681
682 /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
683 pub fn service_string_advice(&self) -> ServiceStringAdvice {
684 self.ssa
685 }
686
687 /// Escape a value string for inclusion in an EDIFACT segment.
688 ///
689 /// Any character in `value` that matches the active element separator,
690 /// component separator, release character, or segment terminator is escaped
691 /// by prefixing it with the release character (default `?`).
692 ///
693 /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
694 /// avoiding an allocation on the fast path.
695 ///
696 /// # Example
697 ///
698 /// ```rust,ignore
699 /// let writer = Writer::new(std::io::sink());
700 /// // '+' must be escaped since it is the default element separator.
701 /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
702 /// ```
703 pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
704 let bytes = value.as_bytes();
705 if find_escape(&self.ssa, bytes).is_none() {
706 return Cow::Borrowed(value);
707 }
708 // Built as a `String` from the start. Assembling a `Vec<u8>` and then
709 // re-validating it needed a fallible conversion whose failure branch was
710 // unreachable, which is exactly the kind of `expect` that has no business
711 // in a library. Every delimiter is single-byte ASCII (enforced by
712 // `ServiceStringAdvice::is_valid`), so each hit lands on a character
713 // boundary and both halves of the split are valid `&str`.
714 let release = self.ssa.release_char as char;
715 let mut out = String::with_capacity(value.len() + 4);
716 let mut last = 0;
717 while let Some(hit) = find_escape(&self.ssa, &bytes[last..]) {
718 let abs = last + hit;
719 out.push_str(&value[last..abs]);
720 out.push(release);
721 out.push(bytes[abs] as char);
722 last = abs + 1;
723 }
724 out.push_str(&value[last..]);
725 Cow::Owned(out)
726 }
727 /// Write only the segment tag bytes — no element separator or terminator.
728 ///
729 /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
730 #[inline]
731 pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
732 self.write_tag(tag)?;
733 self.open_segment_is_unh = tag == "UNH";
734 Ok(())
735 }
736
737 /// Write one element separator byte.
738 #[inline]
739 pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
740 self.inner.write_all(&[self.ssa.element_sep])?;
741 Ok(())
742 }
743
744 /// Write one component separator byte.
745 #[inline]
746 pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
747 self.inner.write_all(&[self.ssa.component_sep])?;
748 Ok(())
749 }
750
751 /// Write one repetition separator byte, or refuse when none is declared.
752 ///
753 /// Emitting the space sentinel would produce output that reads back as a
754 /// single occurrence whose value contains a space — corrupt, and quietly so.
755 #[inline]
756 pub(crate) fn write_repetition_sep(&mut self) -> Result<(), EdifactError> {
757 if !self.ssa.is_repetition_active() {
758 return Err(EdifactError::RepetitionSeparatorNotDeclared);
759 }
760 self.inner.write_all(&[self.ssa.repetition_sep])?;
761 Ok(())
762 }
763
764 /// Write the segment terminator and increment the internal segment counter.
765 #[inline]
766 pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
767 let tag = if self.open_segment_is_unh { "UNH" } else { "" };
768 self.open_segment_is_unh = false;
769 self.end_segment(tag)
770 }
771
772 /// Write text, encoding it into the bound repertoire when there is one.
773 ///
774 /// Callers must only pass slices that start and end on a character boundary.
775 /// Every delimiter is single-byte ASCII (enforced by
776 /// [`ServiceStringAdvice::is_valid`]), so splitting a value at a delimiter
777 /// always satisfies that.
778 #[inline]
779 fn write_text(&mut self, text: &str) -> Result<(), EdifactError> {
780 match self.charset {
781 None => self.inner.write_all(text.as_bytes())?,
782 Some(charset) => self.inner.write_all(&charset.encode(text)?)?,
783 }
784 Ok(())
785 }
786
787 /// Write a value, escaping any delimiter characters.
788 pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
789 let release = self.ssa.release_char;
790 let bytes = value.as_bytes();
791 let mut last = 0;
792 let mut pos = 0;
793 while pos < bytes.len() {
794 let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
795 break;
796 };
797 let abs = pos + hit;
798 if abs > last {
799 self.write_text(&value[last..abs])?;
800 }
801 // The escaped byte is a service character, hence ASCII in every
802 // repertoire — it needs no encoding pass.
803 self.inner.write_all(&[release, bytes[abs]])?;
804 last = abs + 1;
805 pos = abs + 1;
806 }
807 self.write_text(&value[last..])
808 }
809
810 // ── Interchange envelope helpers ──────────────────────────────────────────
811
812 /// Write a `UNB` interchange header segment.
813 ///
814 /// Generates:
815 /// ```text
816 /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
817 /// ```
818 ///
819 /// Composite components (S001 syntax identifier/version, S004 date/time) are
820 /// passed separately rather than pre-joined with `:`, so they are written
821 /// with the writer's *active* component separator and so a literal separator
822 /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
823 /// silently promoted to a component boundary.
824 ///
825 /// Track the `control_ref` — it must be repeated in the matching
826 /// [`end_interchange`](Self::end_interchange) call.
827 ///
828 /// # Example
829 ///
830 /// ```
831 /// use edifact_rs::Writer;
832 /// let mut w = Writer::new(Vec::new());
833 /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
834 /// assert_eq!(
835 /// w.finish()?,
836 /// b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
837 /// );
838 /// # Ok::<(), edifact_rs::EdifactError>(())
839 /// ```
840 ///
841 /// # Errors
842 ///
843 /// Returns [`EdifactError`] if writing fails.
844 #[allow(clippy::too_many_arguments)]
845 pub fn begin_interchange(
846 &mut self,
847 syntax_id: &str,
848 syntax_version: &str,
849 sender: &str,
850 recipient: &str,
851 date: &str,
852 time: &str,
853 control_ref: &str,
854 ) -> Result<(), EdifactError> {
855 // A header that names one repertoire while the body is encoded in another
856 // is the exact silent-corruption failure `with_charset` exists to stop, so
857 // a mismatch is refused rather than written.
858 if let Some(charset) = self.charset {
859 if charset.syntax_identifier() != syntax_id {
860 return Err(EdifactError::CharacterRepertoireMismatch {
861 declared: syntax_id.to_owned(),
862 writer: charset.syntax_identifier(),
863 });
864 }
865 }
866 self.write_composites(
867 "UNB",
868 &[
869 &[syntax_id, syntax_version][..],
870 &[sender][..],
871 &[recipient][..],
872 &[date, time][..],
873 &[control_ref][..],
874 ],
875 )
876 }
877
878 /// Write a `UNH` message header and return a [`MessageWriter`] guard.
879 ///
880 /// The guard tracks the per-message segment count automatically. Call
881 /// [`MessageWriter::finish`] when all message segments have been written — this
882 /// writes the matching `UNT` segment with the correct count. If `finish` is not
883 /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
884 /// are silently discarded on drop; prefer explicit `finish`).
885 ///
886 /// Generates:
887 /// ```text
888 /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
889 /// ```
890 ///
891 /// # Errors
892 ///
893 /// Returns [`EdifactError`] if writing the `UNH` segment fails.
894 pub fn begin_message<'w>(
895 &'w mut self,
896 message_ref: &str,
897 message_type: &str,
898 version: &str,
899 release: &str,
900 controlling_agency: &str,
901 ) -> Result<MessageWriter<'w, W>, EdifactError> {
902 // Build S009 as an explicit composite. Formatting it with a literal `:`
903 // and handing it to `write_simple` produced a single collapsed component
904 // whenever the writer used a non-default component separator.
905 self.write_composites(
906 "UNH",
907 &[
908 &[message_ref][..],
909 &[message_type, version, release, controlling_agency][..],
910 ],
911 )?;
912 // Capture `segment_count` after writing UNH so `MessageWriter` knows
913 // the absolute count that includes UNH.
914 let unh_count = self.segment_count;
915 Ok(MessageWriter {
916 writer: self,
917 message_ref: message_ref.to_owned(),
918 unh_count,
919 finished: false,
920 })
921 }
922
923 /// Write a `UNZ` interchange trailer segment.
924 ///
925 /// `message_count` is the number of `UNH`/`UNT` message pairs in the
926 /// interchange. `control_ref` must match the value passed to
927 /// [`begin_interchange`](Self::begin_interchange).
928 ///
929 /// If you used [`begin_message`](Self::begin_message) for every message in the
930 /// interchange, `message_count` equals the number of times you called that
931 /// method.
932 ///
933 /// # Errors
934 ///
935 /// Returns [`EdifactError`] if writing fails.
936 pub fn end_interchange(
937 &mut self,
938 message_count: u32,
939 control_ref: &str,
940 ) -> Result<(), EdifactError> {
941 let msg_count_str = message_count.to_string();
942 self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
943 }
944}
945
946/// RAII guard for a single EDIFACT message within an interchange.
947///
948/// Obtained from [`Writer::begin_message`]. Writes `UNH` on creation and
949/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
950/// is called or the guard is dropped.
951///
952/// Always prefer calling [`finish`](Self::finish) explicitly so that write
953/// errors can be propagated. The `Drop` impl writes `UNT` as a best-effort
954/// fallback but silently discards I/O errors.
955///
956/// # Example
957///
958/// ```rust,no_run
959/// # use edifact_rs::{Writer, Segment};
960/// # fn example() -> Result<(), edifact_rs::EdifactError> {
961/// let mut writer = Writer::new(Vec::new());
962/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
963/// {
964/// let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
965/// msg.write_simple("BGM", &["220", "PO001", "9"])?;
966/// msg.finish()?;
967/// }
968/// writer.end_interchange(1, "1")?;
969/// # Ok(())
970/// # }
971/// ```
972pub struct MessageWriter<'w, W: Write> {
973 writer: &'w mut Writer<W>,
974 message_ref: String,
975 /// Absolute segment count immediately after `UNH` was written.
976 unh_count: u64,
977 /// Set to `true` once `finish()` has been called to prevent a double-write
978 /// from the `Drop` impl.
979 finished: bool,
980}
981
982impl<W: Write> MessageWriter<'_, W> {
983 /// Write an all-simple segment within this message.
984 ///
985 /// Delegates to [`Writer::write_simple`].
986 ///
987 /// # Errors
988 ///
989 /// Returns [`EdifactError`] if the underlying writer fails.
990 pub fn write_simple<S: AsRef<str>>(
991 &mut self,
992 tag: &str,
993 elements: &[S],
994 ) -> Result<(), EdifactError> {
995 self.writer.write_simple(tag, elements)
996 }
997
998 /// Write an all-composite segment within this message.
999 ///
1000 /// Delegates to [`Writer::write_composites`].
1001 ///
1002 /// # Errors
1003 ///
1004 /// Returns [`EdifactError`] if the underlying writer fails.
1005 pub fn write_composites<E, S>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
1006 where
1007 E: AsRef<[S]>,
1008 S: AsRef<str>,
1009 {
1010 self.writer.write_composites(tag, elements)
1011 }
1012
1013 /// Write a segment mixing simple and composite data elements within this message.
1014 ///
1015 /// Delegates to [`Writer::write_elements`].
1016 ///
1017 /// # Errors
1018 ///
1019 /// Returns [`EdifactError`] if the underlying writer fails.
1020 pub fn write_elements(
1021 &mut self,
1022 tag: &str,
1023 elements: &[DataElement<'_>],
1024 ) -> Result<(), EdifactError> {
1025 self.writer.write_elements(tag, elements)
1026 }
1027
1028 /// Write a fully-typed segment within this message.
1029 ///
1030 /// Delegates to [`Writer::write_segment`].
1031 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
1032 self.writer.write_segment(seg)
1033 }
1034
1035 /// Compute the per-message segment count and write `UNT`, consuming the guard.
1036 ///
1037 /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
1038 /// and `UNT` itself — matching the EDIFACT standard.
1039 ///
1040 /// # Errors
1041 ///
1042 /// Returns [`EdifactError`] if writing the `UNT` segment fails.
1043 pub fn finish(mut self) -> Result<(), EdifactError> {
1044 self.write_unt()?;
1045 self.finished = true;
1046 Ok(())
1047 }
1048
1049 fn write_unt(&mut self) -> Result<(), EdifactError> {
1050 // Segments since UNH: writer.segment_count - unh_count (content only).
1051 // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
1052 let count = self.writer.segment_count - self.unh_count + 2;
1053 let count_str = count.to_string();
1054 self.writer.write_composites(
1055 "UNT",
1056 &[&[count_str.as_str()], &[self.message_ref.as_str()]],
1057 )
1058 }
1059}
1060
1061impl<W: Write> Drop for MessageWriter<'_, W> {
1062 fn drop(&mut self) {
1063 if !self.finished {
1064 // Best-effort: write UNT; errors cannot be propagated from drop.
1065 let _ = self.write_unt();
1066 }
1067 }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073 use crate::model::Element;
1074
1075 /// A non-default UNA whose delimiters share no byte with the defaults.
1076 fn exotic_ssa() -> ServiceStringAdvice {
1077 ServiceStringAdvice {
1078 component_sep: b'|',
1079 element_sep: b'!',
1080 decimal_mark: b',',
1081 release_char: b'#',
1082 repetition_sep: b'*',
1083 segment_term: b'~',
1084 }
1085 }
1086
1087 #[test]
1088 fn unh_composite_uses_the_active_component_separator() {
1089 // S009 must be built as a real composite: a literal `:` in a
1090 // `format!` collapses into one component under a custom UNA.
1091 let mut buf = Vec::new();
1092 {
1093 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1094 let msg = w
1095 .begin_message("1", "ORDERS", "D", "96A", "UN")
1096 .expect("UNH");
1097 msg.finish().expect("UNT");
1098 }
1099 let out = String::from_utf8(buf).unwrap();
1100 assert!(
1101 out.contains("UNH!1!ORDERS|D|96A|UN~"),
1102 "S009 must use `|`, got {out}"
1103 );
1104 }
1105
1106 #[test]
1107 fn round_trips_through_a_custom_una() {
1108 // The library must be able to re-read its own output verbatim.
1109 let mut buf = Vec::new();
1110 {
1111 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1112 w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
1113 .unwrap();
1114 let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1115 msg.write_simple("BGM", &["220"]).unwrap();
1116 msg.finish().unwrap();
1117 w.end_interchange(1, "IC1").unwrap();
1118 }
1119 let segs: Vec<_> = crate::from_bytes(&buf)
1120 .collect::<Result<Vec<_>, _>>()
1121 .expect("own output must reparse");
1122 let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
1123 assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
1124 assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
1125 crate::validate_envelope(&segs).expect("own output must pass envelope validation");
1126 }
1127
1128 #[test]
1129 fn finish_unt_counts_only_the_current_message() {
1130 // `finish_unt` used the writer-lifetime segment total, so a preceding
1131 // UNB inflated DE 0074 and the interchange failed its own validation.
1132 let mut buf = Vec::new();
1133 {
1134 let mut w = Writer::new(&mut buf);
1135 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1136 .unwrap();
1137 w.write_composites("UNH", &[&["1"][..], &["ORDERS", "D", "96A", "UN"][..]])
1138 .unwrap();
1139 w.write_simple("BGM", &["220"]).unwrap();
1140 w.finish_unt("1").unwrap();
1141 }
1142 let out = String::from_utf8(buf).unwrap();
1143 // UNH + BGM + UNT == 3
1144 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1145 }
1146
1147 #[test]
1148 fn repetition_separator_is_escaped_when_declared() {
1149 let mut buf = Vec::new();
1150 {
1151 let mut w = Writer::with_una(
1152 &mut buf,
1153 ServiceStringAdvice {
1154 repetition_sep: b'*',
1155 ..ServiceStringAdvice::default()
1156 },
1157 )
1158 .unwrap();
1159 w.write_composites("FTX", &[&["a*b"]]).unwrap();
1160 }
1161 let out = String::from_utf8(buf).unwrap();
1162 assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
1163 }
1164
1165 #[test]
1166 fn repetition_separator_sentinel_is_not_escaped() {
1167 // Space at UNA position 7 means "not used" and must never be escaped.
1168 let w = Writer::new(std::io::sink());
1169 assert_eq!(w.escape_value("a b"), "a b");
1170 }
1171
1172 #[test]
1173 fn write_composites_escapes_a_literal_component_separator() {
1174 let mut buf = Vec::new();
1175 {
1176 let mut w = Writer::new(&mut buf);
1177 w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
1178 .unwrap();
1179 }
1180 let segs: Vec<_> = crate::from_bytes(&buf)
1181 .collect::<Result<Vec<_>, _>>()
1182 .unwrap();
1183 // The `:` stays inside the value instead of splitting the element.
1184 assert_eq!(
1185 segs[0].get_element(1).unwrap().get_component(0),
1186 Some("ACME:INC")
1187 );
1188 }
1189
1190 #[test]
1191 fn write_elements_mixes_simple_and_composite() {
1192 let mut buf = Vec::new();
1193 {
1194 let mut w = Writer::new(&mut buf);
1195 w.write_elements(
1196 "NAD",
1197 &[
1198 DataElement::Simple("MS"),
1199 DataElement::Composite(&["9900112233445", "", "293"]),
1200 ],
1201 )
1202 .unwrap();
1203 }
1204 assert_eq!(buf, b"NAD+MS+9900112233445::293'");
1205 }
1206
1207 #[test]
1208 fn elements_macro_matches_the_explicit_form() {
1209 let mut macro_buf = Vec::new();
1210 {
1211 let mut w = Writer::new(&mut macro_buf);
1212 w.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1213 .unwrap();
1214 w.write_elements("DTM", elements![["137", "20260101", "102"]])
1215 .unwrap();
1216 }
1217 let mut explicit_buf = Vec::new();
1218 {
1219 let mut w = Writer::new(&mut explicit_buf);
1220 w.write_elements(
1221 "NAD",
1222 &[
1223 DataElement::Simple("MS"),
1224 DataElement::Composite(&["ACME", "", "9"]),
1225 ],
1226 )
1227 .unwrap();
1228 w.write_elements(
1229 "DTM",
1230 &[DataElement::Composite(&["137", "20260101", "102"])],
1231 )
1232 .unwrap();
1233 }
1234 assert_eq!(macro_buf, explicit_buf);
1235 assert_eq!(macro_buf, b"NAD+MS+ACME::9'DTM+137:20260101:102'");
1236 }
1237
1238 #[test]
1239 fn elements_macro_accepts_arbitrary_expressions() {
1240 // Builders emit runtime values, not literals. A `tt`-based macro only
1241 // matched single-token entries, so `qualifier.as_str()` failed to parse
1242 // — which is precisely the shape this macro exists for.
1243 let qualifier = String::from("MS");
1244 let gln = "9900112233445";
1245 let dtm: Vec<&str> = vec!["137", "20260101", "102"];
1246
1247 let mut buf = Vec::new();
1248 {
1249 let mut w = Writer::new(&mut buf);
1250 w.write_elements("NAD", elements![qualifier.as_str(), [gln, "", "293"]])
1251 .unwrap();
1252 w.write_elements("DTM", elements![dtm]).unwrap();
1253 w.write_elements("FTX", elements![qualifier]).unwrap();
1254 w.write_elements("UNS", elements![]).unwrap();
1255 }
1256 assert_eq!(
1257 String::from_utf8(buf).unwrap(),
1258 "NAD+MS+9900112233445::293'DTM+137:20260101:102'FTX+MS'UNS'"
1259 );
1260 }
1261
1262 #[test]
1263 fn write_elements_escapes_a_literal_component_separator() {
1264 // The `:` stays inside the value instead of splitting the element —
1265 // the failure mode of pre-joining components into one string.
1266 let mut buf = Vec::new();
1267 {
1268 let mut w = Writer::new(&mut buf);
1269 w.write_elements(
1270 "NAD",
1271 &[DataElement::Simple("MS"), DataElement::Simple("ACME:INC")],
1272 )
1273 .unwrap();
1274 }
1275 let segs: Vec<_> = crate::from_bytes(&buf)
1276 .collect::<Result<Vec<_>, _>>()
1277 .unwrap();
1278 assert_eq!(
1279 segs[0].get_element(1).unwrap().get_component(0),
1280 Some("ACME:INC")
1281 );
1282 }
1283
1284 #[test]
1285 fn write_elements_uses_the_active_component_separator() {
1286 let mut buf = Vec::new();
1287 {
1288 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
1289 w.write_elements("DTM", elements![["137", "20260101", "102"]])
1290 .unwrap();
1291 }
1292 let out = String::from_utf8(buf).unwrap();
1293 assert!(
1294 out.ends_with("DTM!137|20260101|102~"),
1295 "expected custom delimiters, got {out}"
1296 );
1297 }
1298
1299 #[test]
1300 fn message_writer_counts_write_elements_segments() {
1301 // `MessageWriter` had no mixed-emit delegate, so callers dropped to the
1302 // raw writer and their segments escaped the UNT DE 0074 count.
1303 let mut buf = Vec::new();
1304 {
1305 let mut w = Writer::new(&mut buf);
1306 let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
1307 msg.write_elements("NAD", elements!["MS", ["ACME", "", "9"]])
1308 .unwrap();
1309 msg.write_composites("DTM", &[&["137", "20260101", "102"]])
1310 .unwrap();
1311 msg.finish().unwrap();
1312 }
1313 let out = String::from_utf8(buf).unwrap();
1314 // UNH + NAD + DTM + UNT == 4
1315 assert!(out.contains("UNT+4+1'"), "expected UNT+4, got {out}");
1316 }
1317
1318 #[test]
1319 fn write_segment_records_unh_for_the_unt_count() {
1320 // `write_segment` and `write_composites` did not record the UNH
1321 // marker, so `finish_unt` fell back to the writer-lifetime total and
1322 // DE 0074 came out inflated by every preceding interchange segment.
1323 let mut buf = Vec::new();
1324 {
1325 let mut w = Writer::new(&mut buf);
1326 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1327 .unwrap();
1328 w.write_segment(&Segment::new(
1329 "UNH",
1330 vec![
1331 Element::of(&["1"]),
1332 Element::of(&["ORDERS", "D", "96A", "UN"]),
1333 ],
1334 ))
1335 .unwrap();
1336 w.write_segment(&Segment::new("BGM", vec![Element::of(&["220"])]))
1337 .unwrap();
1338 w.finish_unt("1").unwrap();
1339 }
1340 let out = String::from_utf8(buf).unwrap();
1341 // UNH + BGM + UNT == 3, not 4 (which would count the UNB).
1342 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1343 }
1344
1345 #[test]
1346 fn write_composites_records_unh_for_the_unt_count() {
1347 let mut buf = Vec::new();
1348 {
1349 let mut w = Writer::new(&mut buf);
1350 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
1351 .unwrap();
1352 w.write_composites(
1353 "UNH",
1354 &[
1355 vec!["1".to_owned()],
1356 vec![
1357 "ORDERS".to_owned(),
1358 "D".to_owned(),
1359 "96A".to_owned(),
1360 "UN".to_owned(),
1361 ],
1362 ],
1363 )
1364 .unwrap();
1365 w.write_simple("BGM", &["220"]).unwrap();
1366 w.finish_unt("1").unwrap();
1367 }
1368 let out = String::from_utf8(buf).unwrap();
1369 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
1370 }
1371
1372 #[test]
1373 fn escape_value_handles_multi_byte_text() {
1374 // The old implementation assembled a `Vec<u8>` and re-validated it with
1375 // an `expect`. Escaping around non-ASCII text is the case that made
1376 // that conversion look fallible in the first place.
1377 let w = Writer::new(std::io::sink());
1378 assert_eq!(w.escape_value("Grüße+Köln"), "Grüße?+Köln");
1379 assert_eq!(w.escape_value("Grüße"), "Grüße");
1380 assert!(matches!(w.escape_value("plain"), Cow::Borrowed("plain")));
1381 }
1382
1383 #[test]
1384 fn write_and_parse_simple_segment() {
1385 let segs: Vec<Segment<'static>> = vec![Segment::new(
1386 "BGM",
1387 vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
1388 )];
1389 let bytes = crate::segments_to_bytes(&segs).unwrap();
1390 let s = std::str::from_utf8(&bytes).unwrap();
1391 assert!(s.starts_with("BGM+220+ORDER123'"));
1392 }
1393
1394 #[test]
1395 fn a_tag_the_parser_would_reject_is_never_written() {
1396 // A tag is emitted verbatim — EDIFACT cannot escape one — so writing an
1397 // invalid tag produces bytes that do not reparse as the segment they
1398 // came from. Every write path must refuse before the first byte.
1399 for tag in ["bgm", "BGMX", "BG", "", "B+M", "B'M", "BG1", "BGÜ"] {
1400 let mut writer = Writer::new(Vec::new());
1401 let err = writer
1402 .write_simple(tag, &["220"])
1403 .expect_err("an invalid tag must be refused");
1404 assert!(
1405 matches!(err, EdifactError::InvalidSegmentTag(ref t) if t == tag),
1406 "expected InvalidSegmentTag({tag:?}), got {err:?}",
1407 );
1408 // Nothing reached the sink.
1409 assert!(
1410 writer.finish().expect("finish").is_empty(),
1411 "a refused segment must leave the sink untouched",
1412 );
1413 }
1414 }
1415
1416 #[test]
1417 fn every_write_path_validates_the_tag() {
1418 let bad = "bgm";
1419 macro_rules! refused {
1420 ($call:expr) => {
1421 assert!(
1422 matches!($call, Err(EdifactError::InvalidSegmentTag(_))),
1423 "a write path accepted an invalid tag",
1424 );
1425 };
1426 }
1427
1428 let mut w = Writer::new(Vec::new());
1429 refused!(w.write_simple(bad, &["1"]));
1430 refused!(w.write_composites(bad, &[&["1"][..]]));
1431 refused!(w.write_elements(bad, elements!["1"]));
1432 refused!(w.write_segment(&Segment::new(bad, vec![Element::of(&["1"])])));
1433 }
1434
1435 #[test]
1436 fn whatever_the_writer_emits_the_parser_reads_back() {
1437 // The round-trip property the tag check exists to preserve.
1438 let segments = vec![
1439 Segment::new(
1440 "UNB",
1441 vec![Element::of(&["UNOA", "1"]), Element::of(&["S"])],
1442 ),
1443 Segment::new(
1444 "BGM",
1445 vec![Element::of(&["220"]), Element::of(&["PO?+1'X"])],
1446 ),
1447 Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1448 ];
1449 let bytes = crate::segments_to_bytes(&segments).expect("write");
1450 let reparsed: Vec<_> = crate::from_bytes(&bytes)
1451 .collect::<Result<Vec<_>, _>>()
1452 .expect("everything the writer emits must reparse");
1453
1454 assert_eq!(
1455 reparsed.iter().map(Segment::tag).collect::<Vec<_>>(),
1456 ["UNB", "BGM", "UNZ"],
1457 );
1458 // Delimiters inside a *value* survive, because a value can be escaped.
1459 assert_eq!(reparsed[1].element_str(1), Some("PO?+1'X"));
1460 }
1461
1462 #[test]
1463 fn release_char_escaped() {
1464 let segs: Vec<Segment<'static>> = vec![Segment::new(
1465 "FTX",
1466 vec![Element::of(&["value+with+delimiters"])],
1467 )];
1468 let bytes = crate::segments_to_bytes(&segs).unwrap();
1469 let s = std::str::from_utf8(&bytes).unwrap();
1470 // The `+` in the value must be escaped as `?+`
1471 assert!(s.contains("?+"), "escape missing: {s}");
1472 }
1473
1474 #[test]
1475 fn round_trip_preserves_values() {
1476 let segs: Vec<Segment<'static>> = vec![
1477 Segment::new(
1478 "UNB",
1479 vec![
1480 Element::of(&["UNOA", "1"]),
1481 Element::of(&["SENDER"]),
1482 Element::of(&["RECEIVER"]),
1483 ],
1484 ),
1485 Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
1486 ];
1487 let bytes = crate::segments_to_bytes(&segs).unwrap();
1488 let rt: Vec<crate::OwnedSegment> = crate::from_reader(std::io::Cursor::new(&bytes))
1489 .collect::<Result<Vec<_>, _>>()
1490 .expect("round-trip parse failed");
1491 assert_eq!(rt[0].tag, "UNB");
1492 assert_eq!(rt[0].element_str(0), Some("UNOA"));
1493 assert_eq!(rt[1].tag, "UNZ");
1494 }
1495
1496 /// Verify that `Writer::with_una` uses the configured delimiters throughout,
1497 /// and that `write_composites` (the delimiter-agnostic API) produces correct
1498 /// component separators even with a non-default UNA.
1499 #[test]
1500 fn with_una_non_default_delimiters() {
1501 use crate::tokenizer::ServiceStringAdvice;
1502
1503 // Custom UNA: comp_sep=| elem_sep=! esc=? dec_mark=, rep_sep=* seg_term=~
1504 let ssa = ServiceStringAdvice {
1505 component_sep: b'|',
1506 element_sep: b'!',
1507 release_char: b'?',
1508 decimal_mark: b',',
1509 repetition_sep: b'*',
1510 segment_term: b'~',
1511 };
1512
1513 let buf = Vec::new();
1514 let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
1515
1516 // write_composites: pre-split; no hard-coded `:` in element strings
1517 writer
1518 .write_composites(
1519 "BGM",
1520 &[
1521 vec!["220".to_owned(), "SUB1".to_owned()],
1522 vec!["PO1".to_owned()],
1523 ],
1524 )
1525 .expect("write failed");
1526
1527 let out = writer.finish().expect("finish failed");
1528 let s = std::str::from_utf8(&out).unwrap();
1529
1530 // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
1531 // The writer also emits a UNA header when with_una is used.
1532 assert!(s.contains("BGM"), "BGM segment missing: {s}");
1533 // Slice after UNA so assertions target segment output, not UNA header bytes.
1534 let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
1535 assert!(
1536 after_una.contains('!'),
1537 "missing element sep in segment: {after_una}"
1538 );
1539 assert!(
1540 after_una.contains('|'),
1541 "missing component sep in segment: {after_una}"
1542 );
1543 assert!(
1544 after_una.ends_with('~'),
1545 "missing segment term in segment: {after_una}"
1546 );
1547 // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
1548 assert!(s.contains(','), "missing decimal mark in UNA: {s}");
1549 assert!(!s.contains('+'), "default element sep leaked: {s}");
1550 assert!(!s.contains(':'), "default component sep leaked: {s}");
1551 // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
1552 assert!(
1553 !after_una.contains('\''),
1554 "default segment term leaked after UNA: {after_una}"
1555 );
1556 }
1557}