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/// Streaming EDIFACT writer.
8///
9/// Wraps any [`Write`] implementation and serializes segments one at a time.
10/// Call [`Writer::finish`] to flush and get the underlying writer back.
11pub struct Writer<W: Write> {
12 inner: W,
13 ssa: ServiceStringAdvice,
14 /// Running count of segments written. `u64` to prevent silent overflow on
15 /// pathological inputs (a `u32` would wrap after ~4 billion segments).
16 segment_count: u64,
17 /// `segment_count` as of the most recent `UNH`, used by [`Writer::finish_unt`]
18 /// to derive a per-message DE 0074 rather than a writer-lifetime total.
19 message_start_count: u64,
20}
21
22/// Return the offset of the first byte in `hay` that must be release-escaped.
23///
24/// The escape set is the four splitting delimiters plus the repetition separator
25/// when the active UNA declares one. A space at UNA position 7 is the
26/// conventional "not used" sentinel and is never escaped.
27#[inline]
28fn find_escape(ssa: &ServiceStringAdvice, hay: &[u8]) -> Option<usize> {
29 let first = memchr::memchr3(ssa.element_sep, ssa.component_sep, ssa.release_char, hay);
30 let second = if ssa.repetition_sep == b' ' {
31 memchr::memchr(ssa.segment_term, hay)
32 } else {
33 memchr::memchr2(ssa.segment_term, ssa.repetition_sep, hay)
34 };
35 match (first, second) {
36 (None, None) => None,
37 (Some(a), None) => Some(a),
38 (None, Some(b)) => Some(b),
39 (Some(a), Some(b)) => Some(a.min(b)),
40 }
41}
42
43impl<W: Write> Writer<W> {
44 /// Create a new writer with default EDIFACT delimiters.
45 pub fn new(inner: W) -> Self {
46 Self {
47 inner,
48 ssa: ServiceStringAdvice::default(),
49 segment_count: 0,
50 message_start_count: 0,
51 }
52 }
53
54 /// Create a writer with custom delimiters and write a UNA segment first.
55 pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
56 // All five active service characters must be mutually distinct, non-whitespace,
57 // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
58 if !ssa.is_valid() {
59 return Err(EdifactError::InvalidUna);
60 }
61 // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
62 let una = [
63 b'U',
64 b'N',
65 b'A',
66 ssa.component_sep,
67 ssa.element_sep,
68 ssa.decimal_mark,
69 ssa.release_char,
70 ssa.repetition_sep,
71 ssa.segment_term,
72 ];
73 inner.write_all(&una)?;
74 Ok(Self {
75 inner,
76 ssa,
77 segment_count: 0,
78 message_start_count: 0,
79 })
80 }
81
82 /// Write a single segment.
83 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
84 // Tag
85 self.inner.write_all(seg.tag.as_bytes())?;
86
87 for element in &seg.elements {
88 // Element separator
89 self.inner.write_all(&[self.ssa.element_sep])?;
90 let mut first_component = true;
91 for (component, _) in &element.components {
92 if !first_component {
93 self.inner.write_all(&[self.ssa.component_sep])?;
94 }
95 first_component = false;
96 self.write_escaped(component)?;
97 }
98 }
99
100 // Segment terminator
101 self.inner.write_all(&[self.ssa.segment_term])?;
102 self.segment_count += 1;
103 Ok(())
104 }
105
106 /// Write a raw segment from tag + element string slices.
107 ///
108 /// Each element string is split on the **active component-separator byte** from the
109 /// configured [`ServiceStringAdvice`][crate::ServiceStringAdvice] to identify component
110 /// boundaries. The default component separator is `:` (0x3A), but this can differ when a
111 /// non-default `UNA` string was used to construct the writer.
112 ///
113 /// # Delimiter dependency
114 ///
115 /// Callers that embed the literal `:` character in element strings rely on `:` being
116 /// the component separator. When the writer uses a non-default delimiter set, `:` will
117 /// **not** be treated as a component boundary and the segment will be written incorrectly.
118 ///
119 /// **UTF-8 safety**: EDIFACT syntax requires all delimiter bytes to be single-byte ASCII
120 /// characters (values 0x00–0x7F). Non-ASCII delimiter bytes would bisect multi-byte UTF-8
121 /// sequences in data values and produce malformed output. All fields of
122 /// [`ServiceStringAdvice`][crate::ServiceStringAdvice] must therefore hold ASCII byte values.
123 ///
124 /// To produce correct output regardless of the active delimiter, prefer
125 /// [`Self::write_segment_parts`] which accepts pre-split component slices.
126 pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
127 self.inner.write_all(tag.as_bytes())?;
128 let comp_sep = self.ssa.component_sep;
129 for el in elements {
130 self.inner.write_all(&[self.ssa.element_sep])?;
131 // Byte-level split: EDIFACT delimiters are always single bytes.
132 let mut parts = el.as_bytes().split(|&b| b == comp_sep);
133 if let Some(first) = parts.next() {
134 // INVARIANT: input is valid UTF-8 and we split on a single-byte ASCII
135 // delimiter, so each part remains a valid UTF-8 slice.
136 self.write_escaped(
137 std::str::from_utf8(first).map_err(|_| EdifactError::InvalidUtf8)?,
138 )?;
139 }
140 for part in parts {
141 self.inner.write_all(&[comp_sep])?;
142 self.write_escaped(
143 std::str::from_utf8(part).map_err(|_| EdifactError::InvalidUtf8)?,
144 )?;
145 }
146 }
147 self.inner.write_all(&[self.ssa.segment_term])?;
148 if tag == "UNH" {
149 self.message_start_count = self.segment_count;
150 }
151 self.segment_count += 1;
152 Ok(())
153 }
154
155 /// Write a segment from a tag and pre-split element/component data.
156 ///
157 /// `elements` is a slice of elements; each element is a sequence of component strings.
158 /// This avoids the lifetime constraints of [`Self::write_segment`] when building
159 /// segments from runtime-owned data (e.g. inside [`crate::WriterEmitter`]).
160 pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
161 where
162 E: AsRef<[String]>,
163 {
164 self.inner.write_all(tag.as_bytes())?;
165 for element in elements {
166 self.inner.write_all(&[self.ssa.element_sep])?;
167 let mut first = true;
168 for comp in element.as_ref() {
169 if !first {
170 self.inner.write_all(&[self.ssa.component_sep])?;
171 }
172 first = false;
173 self.write_escaped(comp.as_str())?;
174 }
175 }
176 self.inner.write_all(&[self.ssa.segment_term])?;
177 self.segment_count += 1;
178 Ok(())
179 }
180
181 /// Write a segment from a tag and borrowed element/component slices.
182 ///
183 /// Unlike [`Self::write_raw`], component boundaries are given explicitly
184 /// rather than inferred by splitting on the active component separator, so
185 /// values containing a literal separator byte are escaped instead of being
186 /// silently reinterpreted as a composite boundary. Unlike
187 /// [`Self::write_segment_parts`], no `String` allocation is required.
188 ///
189 /// # Example
190 ///
191 /// ```
192 /// use edifact_rs::Writer;
193 /// let mut w = Writer::new(Vec::new());
194 /// // The `:` inside the sender id stays part of the value.
195 /// w.write_composites("NAD", &[&["MS"][..], &["ACME:INC"][..]])?;
196 /// assert_eq!(w.finish()?, b"NAD+MS+ACME?:INC'".to_vec());
197 /// # Ok::<(), edifact_rs::EdifactError>(())
198 /// ```
199 ///
200 /// # Errors
201 ///
202 /// Returns [`EdifactError`] if the underlying writer fails.
203 pub fn write_composites(
204 &mut self,
205 tag: &str,
206 elements: &[&[&str]],
207 ) -> Result<(), EdifactError> {
208 self.inner.write_all(tag.as_bytes())?;
209 for element in elements {
210 self.inner.write_all(&[self.ssa.element_sep])?;
211 for (i, comp) in element.iter().enumerate() {
212 if i > 0 {
213 self.inner.write_all(&[self.ssa.component_sep])?;
214 }
215 self.write_escaped(comp)?;
216 }
217 }
218 self.inner.write_all(&[self.ssa.segment_term])?;
219 if tag == "UNH" {
220 self.message_start_count = self.segment_count;
221 }
222 self.segment_count += 1;
223 Ok(())
224 }
225
226 /// Flush and return the underlying writer.
227 pub fn finish(mut self) -> Result<W, EdifactError> {
228 self.inner.flush()?;
229 Ok(self.inner)
230 }
231
232 /// Write the `UNT` segment and return the inner writer.
233 ///
234 /// The count written into `UNT` DE 0074 covers the current message only:
235 /// `UNH`, every segment written since it, and `UNT` itself. Segments written
236 /// before the message's `UNH` — an interchange-level `UNB`, or a preceding
237 /// message — are excluded, as EDIFACT requires.
238 ///
239 /// If no `UNH` has been written, the count falls back to every segment
240 /// written so far plus one.
241 ///
242 /// # Errors
243 ///
244 /// Returns an error if writing fails. Do **not** call [`write_raw`][Self::write_raw] or
245 /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
246 pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
247 // DE 0074 counts UNH + content + UNT. `message_start_count` is the
248 // absolute segment count immediately after UNH, so content is
249 // `segment_count - message_start_count` and the total adds UNH and UNT.
250 let count = self.segment_count - self.message_start_count + 1;
251 let count_str = count.to_string();
252 self.write_composites("UNT", &[&[count_str.as_str()], &[message_ref]])?;
253 self.finish()
254 }
255
256 /// Returns the total number of segments written so far.
257 pub fn segment_count(&self) -> u64 {
258 self.segment_count
259 }
260
261 /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
262 pub fn service_string_advice(&self) -> ServiceStringAdvice {
263 self.ssa
264 }
265
266 /// Escape a value string for inclusion in an EDIFACT segment.
267 ///
268 /// Any character in `value` that matches the active element separator,
269 /// component separator, release character, or segment terminator is escaped
270 /// by prefixing it with the release character (default `?`).
271 ///
272 /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
273 /// avoiding an allocation on the fast path.
274 ///
275 /// # Example
276 ///
277 /// ```rust,ignore
278 /// let writer = Writer::new(std::io::sink());
279 /// // '+' must be escaped since it is the default element separator.
280 /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
281 /// ```
282 pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
283 let release = self.ssa.release_char;
284 let bytes = value.as_bytes();
285 if find_escape(&self.ssa, bytes).is_none() {
286 return Cow::Borrowed(value);
287 }
288 let mut out = Vec::with_capacity(value.len() + 4);
289 let mut last = 0;
290 let mut pos = 0;
291 while pos < bytes.len() {
292 let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
293 break;
294 };
295 let abs = pos + hit;
296 out.extend_from_slice(&bytes[last..abs]);
297 out.push(release);
298 out.push(bytes[abs]);
299 last = abs + 1;
300 pos = abs + 1;
301 }
302 out.extend_from_slice(&bytes[last..]);
303 // SAFETY:
304 // 1. `value` is a valid `&str`, so `bytes` is valid UTF-8 to start.
305 // 2. `self.ssa.release_char` is a single-byte ASCII value (0x21–0x7E),
306 // enforced at construction time by `ServiceStringAdvice::is_valid()`
307 // (called in `Writer::with_una`; the default SSA hardcodes `?` = 0x3F).
308 // Inserting a single ASCII byte cannot split or corrupt a multi-byte
309 // UTF-8 sequence, because ASCII bytes always have the high bit clear
310 // while continuation bytes of multi-byte sequences always have the high
311 // bit set (0x80–0xBF).
312 // 3. All other bytes are copied verbatim from the valid UTF-8 source.
313 Cow::Owned(
314 String::from_utf8(out).expect(
315 "escape_value: output is not valid UTF-8; this is a bug in the escape logic",
316 ),
317 )
318 }
319 /// Write only the segment tag bytes — no element separator or terminator.
320 ///
321 /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
322 #[inline]
323 pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
324 self.inner.write_all(tag.as_bytes())?;
325 Ok(())
326 }
327
328 /// Write one element separator byte.
329 #[inline]
330 pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
331 self.inner.write_all(&[self.ssa.element_sep])?;
332 Ok(())
333 }
334
335 /// Write one component separator byte.
336 #[inline]
337 pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
338 self.inner.write_all(&[self.ssa.component_sep])?;
339 Ok(())
340 }
341
342 /// Write the segment terminator and increment the internal segment counter.
343 #[inline]
344 pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
345 self.inner.write_all(&[self.ssa.segment_term])?;
346 self.segment_count += 1;
347 Ok(())
348 }
349
350 /// Write a value, escaping any delimiter characters.
351 pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
352 let release = self.ssa.release_char;
353 let bytes = value.as_bytes();
354 let mut last = 0;
355 let mut pos = 0;
356 while pos < bytes.len() {
357 let Some(hit) = find_escape(&self.ssa, &bytes[pos..]) else {
358 break;
359 };
360 let abs = pos + hit;
361 if abs > last {
362 self.inner.write_all(&bytes[last..abs])?;
363 }
364 self.inner.write_all(&[release, bytes[abs]])?;
365 last = abs + 1;
366 pos = abs + 1;
367 }
368 self.inner.write_all(&bytes[last..])?;
369 Ok(())
370 }
371
372 // ── Interchange envelope helpers ──────────────────────────────────────────
373
374 /// Write a `UNB` interchange header segment.
375 ///
376 /// Generates:
377 /// ```text
378 /// UNB+<syntax_id>:<syntax_version>+<sender>+<recipient>+<date>:<time>+<control_ref>'
379 /// ```
380 ///
381 /// Composite components (S001 syntax identifier/version, S004 date/time) are
382 /// passed separately rather than pre-joined with `:`, so they are written
383 /// with the writer's *active* component separator and so a literal separator
384 /// inside `sender`, `recipient`, or `control_ref` is escaped rather than
385 /// silently promoted to a component boundary.
386 ///
387 /// Track the `control_ref` — it must be repeated in the matching
388 /// [`end_interchange`](Self::end_interchange) call.
389 ///
390 /// # Example
391 ///
392 /// ```
393 /// use edifact_rs::Writer;
394 /// let mut w = Writer::new(Vec::new());
395 /// w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")?;
396 /// assert_eq!(
397 /// w.finish()?,
398 /// b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+IC1'".to_vec(),
399 /// );
400 /// # Ok::<(), edifact_rs::EdifactError>(())
401 /// ```
402 ///
403 /// # Errors
404 ///
405 /// Returns [`EdifactError`] if writing fails.
406 #[allow(clippy::too_many_arguments)]
407 pub fn begin_interchange(
408 &mut self,
409 syntax_id: &str,
410 syntax_version: &str,
411 sender: &str,
412 recipient: &str,
413 date: &str,
414 time: &str,
415 control_ref: &str,
416 ) -> Result<(), EdifactError> {
417 self.write_composites(
418 "UNB",
419 &[
420 &[syntax_id, syntax_version],
421 &[sender],
422 &[recipient],
423 &[date, time],
424 &[control_ref],
425 ],
426 )
427 }
428
429 /// Write a `UNH` message header and return a [`MessageWriter`] guard.
430 ///
431 /// The guard tracks the per-message segment count automatically. Call
432 /// [`MessageWriter::finish`] when all message segments have been written — this
433 /// writes the matching `UNT` segment with the correct count. If `finish` is not
434 /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
435 /// are silently discarded on drop; prefer explicit `finish`).
436 ///
437 /// Generates:
438 /// ```text
439 /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
440 /// ```
441 ///
442 /// # Errors
443 ///
444 /// Returns [`EdifactError`] if writing the `UNH` segment fails.
445 pub fn begin_message<'w>(
446 &'w mut self,
447 message_ref: &str,
448 message_type: &str,
449 version: &str,
450 release: &str,
451 controlling_agency: &str,
452 ) -> Result<MessageWriter<'w, W>, EdifactError> {
453 // Build S009 as an explicit composite. Formatting it with a literal `:`
454 // and handing it to `write_raw` produced a single collapsed component
455 // whenever the writer used a non-default component separator.
456 self.write_composites(
457 "UNH",
458 &[
459 &[message_ref],
460 &[message_type, version, release, controlling_agency],
461 ],
462 )?;
463 // Capture `segment_count` after writing UNH so `MessageWriter` knows
464 // the absolute count that includes UNH.
465 let unh_count = self.segment_count;
466 Ok(MessageWriter {
467 writer: self,
468 message_ref: message_ref.to_owned(),
469 unh_count,
470 finished: false,
471 })
472 }
473
474 /// Write a `UNZ` interchange trailer segment.
475 ///
476 /// `message_count` is the number of `UNH`/`UNT` message pairs in the
477 /// interchange. `control_ref` must match the value passed to
478 /// [`begin_interchange`](Self::begin_interchange).
479 ///
480 /// If you used [`begin_message`](Self::begin_message) for every message in the
481 /// interchange, `message_count` equals the number of times you called that
482 /// method.
483 ///
484 /// # Errors
485 ///
486 /// Returns [`EdifactError`] if writing fails.
487 pub fn end_interchange(
488 &mut self,
489 message_count: u32,
490 control_ref: &str,
491 ) -> Result<(), EdifactError> {
492 let msg_count_str = message_count.to_string();
493 self.write_composites("UNZ", &[&[msg_count_str.as_str()], &[control_ref]])
494 }
495}
496
497/// RAII guard for a single EDIFACT message within an interchange.
498///
499/// Obtained from [`Writer::begin_message`]. Writes `UNH` on creation and
500/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
501/// is called or the guard is dropped.
502///
503/// Always prefer calling [`finish`](Self::finish) explicitly so that write
504/// errors can be propagated. The `Drop` impl writes `UNT` as a best-effort
505/// fallback but silently discards I/O errors.
506///
507/// # Example
508///
509/// ```rust,no_run
510/// # use edifact_rs::{Writer, Segment};
511/// # fn example() -> Result<(), edifact_rs::EdifactError> {
512/// let mut writer = Writer::new(Vec::new());
513/// writer.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "1")?;
514/// {
515/// let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
516/// msg.write_raw("BGM", &["220", "PO001", "9"])?;
517/// msg.finish()?;
518/// }
519/// writer.end_interchange(1, "1")?;
520/// # Ok(())
521/// # }
522/// ```
523pub struct MessageWriter<'w, W: Write> {
524 writer: &'w mut Writer<W>,
525 message_ref: String,
526 /// Absolute segment count immediately after `UNH` was written.
527 unh_count: u64,
528 /// Set to `true` once `finish()` has been called to prevent a double-write
529 /// from the `Drop` impl.
530 finished: bool,
531}
532
533impl<W: Write> MessageWriter<'_, W> {
534 /// Write a segment within this message.
535 ///
536 /// Delegates to [`Writer::write_raw`].
537 pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
538 self.writer.write_raw(tag, elements)
539 }
540
541 /// Write a fully-typed segment within this message.
542 ///
543 /// Delegates to [`Writer::write_segment`].
544 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
545 self.writer.write_segment(seg)
546 }
547
548 /// Compute the per-message segment count and write `UNT`, consuming the guard.
549 ///
550 /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
551 /// and `UNT` itself — matching the EDIFACT standard.
552 ///
553 /// # Errors
554 ///
555 /// Returns [`EdifactError`] if writing the `UNT` segment fails.
556 pub fn finish(mut self) -> Result<(), EdifactError> {
557 self.write_unt()?;
558 self.finished = true;
559 Ok(())
560 }
561
562 fn write_unt(&mut self) -> Result<(), EdifactError> {
563 // Segments since UNH: writer.segment_count - unh_count (content only).
564 // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
565 let count = self.writer.segment_count - self.unh_count + 2;
566 let count_str = count.to_string();
567 self.writer.write_composites(
568 "UNT",
569 &[&[count_str.as_str()], &[self.message_ref.as_str()]],
570 )
571 }
572}
573
574impl<W: Write> Drop for MessageWriter<'_, W> {
575 fn drop(&mut self) {
576 if !self.finished {
577 // Best-effort: write UNT; errors cannot be propagated from drop.
578 let _ = self.write_unt();
579 }
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::model::Element;
587
588 /// A non-default UNA whose delimiters share no byte with the defaults.
589 fn exotic_ssa() -> ServiceStringAdvice {
590 ServiceStringAdvice {
591 component_sep: b'|',
592 element_sep: b'!',
593 decimal_mark: b',',
594 release_char: b'#',
595 repetition_sep: b'*',
596 segment_term: b'~',
597 }
598 }
599
600 #[test]
601 fn unh_composite_uses_the_active_component_separator() {
602 // `begin_message` used to `format!` the S009 composite with a literal
603 // `:`, collapsing it into one component under a custom UNA.
604 let mut buf = Vec::new();
605 {
606 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
607 let msg = w
608 .begin_message("1", "ORDERS", "D", "96A", "UN")
609 .expect("UNH");
610 msg.finish().expect("UNT");
611 }
612 let out = String::from_utf8(buf).unwrap();
613 assert!(
614 out.contains("UNH!1!ORDERS|D|96A|UN~"),
615 "S009 must use `|`, got {out}"
616 );
617 }
618
619 #[test]
620 fn round_trips_through_a_custom_una() {
621 // The library must be able to re-read its own output verbatim.
622 let mut buf = Vec::new();
623 {
624 let mut w = Writer::with_una(&mut buf, exotic_ssa()).unwrap();
625 w.begin_interchange("UNOA", "1", "SENDER", "RECEIVER", "200101", "0900", "IC1")
626 .unwrap();
627 let mut msg = w.begin_message("1", "ORDERS", "D", "96A", "UN").unwrap();
628 msg.write_raw("BGM", &["220"]).unwrap();
629 msg.finish().unwrap();
630 w.end_interchange(1, "IC1").unwrap();
631 }
632 let segs: Vec<_> = crate::from_bytes(&buf)
633 .collect::<Result<Vec<_>, _>>()
634 .expect("own output must reparse");
635 let unh = segs.iter().find(|s| s.tag == "UNH").unwrap();
636 assert_eq!(unh.get_element(1).unwrap().get_component(0), Some("ORDERS"));
637 assert_eq!(unh.get_element(1).unwrap().get_component(2), Some("96A"));
638 crate::validate_envelope(&segs).expect("own output must pass envelope validation");
639 }
640
641 #[test]
642 fn finish_unt_counts_only_the_current_message() {
643 // `finish_unt` used the writer-lifetime segment total, so a preceding
644 // UNB inflated DE 0074 and the interchange failed its own validation.
645 let mut buf = Vec::new();
646 {
647 let mut w = Writer::new(&mut buf);
648 w.begin_interchange("UNOA", "1", "S", "R", "200101", "0900", "IC1")
649 .unwrap();
650 w.write_composites("UNH", &[&["1"], &["ORDERS", "D", "96A", "UN"]])
651 .unwrap();
652 w.write_raw("BGM", &["220"]).unwrap();
653 w.finish_unt("1").unwrap();
654 }
655 let out = String::from_utf8(buf).unwrap();
656 // UNH + BGM + UNT == 3
657 assert!(out.contains("UNT+3+1'"), "expected UNT+3, got {out}");
658 }
659
660 #[test]
661 fn repetition_separator_is_escaped_when_declared() {
662 let mut buf = Vec::new();
663 {
664 let mut w = Writer::with_una(
665 &mut buf,
666 ServiceStringAdvice {
667 repetition_sep: b'*',
668 ..ServiceStringAdvice::default()
669 },
670 )
671 .unwrap();
672 w.write_composites("FTX", &[&["a*b"]]).unwrap();
673 }
674 let out = String::from_utf8(buf).unwrap();
675 assert!(out.ends_with("FTX+a?*b'"), "rep-sep unescaped in {out}");
676 }
677
678 #[test]
679 fn repetition_separator_sentinel_is_not_escaped() {
680 // Space at UNA position 7 means "not used" and must never be escaped.
681 let w = Writer::new(std::io::sink());
682 assert_eq!(w.escape_value("a b"), "a b");
683 }
684
685 #[test]
686 fn write_composites_escapes_a_literal_component_separator() {
687 let mut buf = Vec::new();
688 {
689 let mut w = Writer::new(&mut buf);
690 w.write_composites("NAD", &[&["MS"], &["ACME:INC"]])
691 .unwrap();
692 }
693 let segs: Vec<_> = crate::from_bytes(&buf)
694 .collect::<Result<Vec<_>, _>>()
695 .unwrap();
696 // The `:` stays inside the value instead of splitting the element.
697 assert_eq!(
698 segs[0].get_element(1).unwrap().get_component(0),
699 Some("ACME:INC")
700 );
701 }
702
703 #[test]
704 fn write_and_parse_simple_segment() {
705 let segs: Vec<Segment<'static>> = vec![Segment::new(
706 "BGM",
707 vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
708 )];
709 let bytes = crate::segments_to_bytes(&segs).unwrap();
710 let s = std::str::from_utf8(&bytes).unwrap();
711 assert!(s.starts_with("BGM+220+ORDER123'"));
712 }
713
714 #[test]
715 fn release_char_escaped() {
716 let segs: Vec<Segment<'static>> = vec![Segment::new(
717 "FTX",
718 vec![Element::of(&["value+with+delimiters"])],
719 )];
720 let bytes = crate::segments_to_bytes(&segs).unwrap();
721 let s = std::str::from_utf8(&bytes).unwrap();
722 // The `+` in the value must be escaped as `?+`
723 assert!(s.contains("?+"), "escape missing: {s}");
724 }
725
726 #[test]
727 fn round_trip_preserves_values() {
728 let segs: Vec<Segment<'static>> = vec![
729 Segment::new(
730 "UNB",
731 vec![
732 Element::of(&["UNOA", "1"]),
733 Element::of(&["SENDER"]),
734 Element::of(&["RECEIVER"]),
735 ],
736 ),
737 Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
738 ];
739 let bytes = crate::segments_to_bytes(&segs).unwrap();
740 let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
741 .expect("round-trip parse failed");
742 assert_eq!(rt[0].tag, "UNB");
743 assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
744 assert_eq!(rt[1].tag, "UNZ");
745 }
746
747 /// Verify that `Writer::with_una` uses the configured delimiters throughout,
748 /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
749 /// component separators even with a non-default UNA.
750 #[test]
751 fn with_una_non_default_delimiters() {
752 use crate::tokenizer::ServiceStringAdvice;
753
754 // Custom UNA: comp_sep=| elem_sep=! esc=? dec_mark=, rep_sep=* seg_term=~
755 let ssa = ServiceStringAdvice {
756 component_sep: b'|',
757 element_sep: b'!',
758 release_char: b'?',
759 decimal_mark: b',',
760 repetition_sep: b'*',
761 segment_term: b'~',
762 };
763
764 let buf = Vec::new();
765 let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
766
767 // write_segment_parts: pre-split; no hard-coded `:` in element strings
768 writer
769 .write_segment_parts(
770 "BGM",
771 &[
772 vec!["220".to_owned(), "SUB1".to_owned()],
773 vec!["PO1".to_owned()],
774 ],
775 )
776 .expect("write failed");
777
778 let out = writer.finish().expect("finish failed");
779 let s = std::str::from_utf8(&out).unwrap();
780
781 // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
782 // The writer also emits a UNA header when with_una is used.
783 assert!(s.contains("BGM"), "BGM segment missing: {s}");
784 // Slice after UNA so assertions target segment output, not UNA header bytes.
785 let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
786 assert!(
787 after_una.contains('!'),
788 "missing element sep in segment: {after_una}"
789 );
790 assert!(
791 after_una.contains('|'),
792 "missing component sep in segment: {after_una}"
793 );
794 assert!(
795 after_una.ends_with('~'),
796 "missing segment term in segment: {after_una}"
797 );
798 // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
799 assert!(s.contains(','), "missing decimal mark in UNA: {s}");
800 assert!(!s.contains('+'), "default element sep leaked: {s}");
801 assert!(!s.contains(':'), "default component sep leaked: {s}");
802 // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
803 assert!(
804 !after_una.contains('\''),
805 "default segment term leaked after UNA: {after_una}"
806 );
807 }
808}