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}
18
19impl<W: Write> Writer<W> {
20 /// Create a new writer with default EDIFACT delimiters.
21 pub fn new(inner: W) -> Self {
22 Self {
23 inner,
24 ssa: ServiceStringAdvice::default(),
25 segment_count: 0,
26 }
27 }
28
29 /// Create a writer with custom delimiters and write a UNA segment first.
30 pub fn with_una(mut inner: W, ssa: ServiceStringAdvice) -> Result<Self, EdifactError> {
31 // All five active service characters must be mutually distinct, non-whitespace,
32 // and within the ASCII range so they never bisect multi-byte UTF-8 sequences.
33 if !ssa.is_valid() {
34 return Err(EdifactError::InvalidUna);
35 }
36 // UNA: component_sep, element_sep, decimal_mark, release_char, repetition_sep, segment_term
37 let una = [
38 b'U',
39 b'N',
40 b'A',
41 ssa.component_sep,
42 ssa.element_sep,
43 ssa.decimal_mark,
44 ssa.release_char,
45 ssa.repetition_sep,
46 ssa.segment_term,
47 ];
48 inner.write_all(&una)?;
49 Ok(Self {
50 inner,
51 ssa,
52 segment_count: 0,
53 })
54 }
55
56 /// Write a single segment.
57 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
58 // Tag
59 self.inner.write_all(seg.tag.as_bytes())?;
60
61 for element in &seg.elements {
62 // Element separator
63 self.inner.write_all(&[self.ssa.element_sep])?;
64 let mut first_component = true;
65 for (component, _) in &element.components {
66 if !first_component {
67 self.inner.write_all(&[self.ssa.component_sep])?;
68 }
69 first_component = false;
70 self.write_escaped(component)?;
71 }
72 }
73
74 // Segment terminator
75 self.inner.write_all(&[self.ssa.segment_term])?;
76 self.segment_count += 1;
77 Ok(())
78 }
79
80 /// Write a raw segment from tag + element string slices.
81 ///
82 /// Each element string is split on the **active component-separator byte** from the
83 /// configured [`ServiceStringAdvice`][crate::ServiceStringAdvice] to identify component
84 /// boundaries. The default component separator is `:` (0x3A), but this can differ when a
85 /// non-default `UNA` string was used to construct the writer.
86 ///
87 /// # Delimiter dependency
88 ///
89 /// Callers that embed the literal `:` character in element strings rely on `:` being
90 /// the component separator. When the writer uses a non-default delimiter set, `:` will
91 /// **not** be treated as a component boundary and the segment will be written incorrectly.
92 ///
93 /// **UTF-8 safety**: EDIFACT syntax requires all delimiter bytes to be single-byte ASCII
94 /// characters (values 0x00–0x7F). Non-ASCII delimiter bytes would bisect multi-byte UTF-8
95 /// sequences in data values and produce malformed output. All fields of
96 /// [`ServiceStringAdvice`][crate::ServiceStringAdvice] must therefore hold ASCII byte values.
97 ///
98 /// To produce correct output regardless of the active delimiter, prefer
99 /// [`Self::write_segment_parts`] which accepts pre-split component slices.
100 pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
101 self.inner.write_all(tag.as_bytes())?;
102 let comp_sep = self.ssa.component_sep;
103 for el in elements {
104 self.inner.write_all(&[self.ssa.element_sep])?;
105 // Byte-level split: EDIFACT delimiters are always single bytes.
106 let mut parts = el.as_bytes().split(|&b| b == comp_sep);
107 if let Some(first) = parts.next() {
108 // INVARIANT: input is valid UTF-8 and we split on a single-byte ASCII
109 // delimiter, so each part remains a valid UTF-8 slice.
110 self.write_escaped(
111 std::str::from_utf8(first).map_err(|_| EdifactError::InvalidUtf8)?,
112 )?;
113 }
114 for part in parts {
115 self.inner.write_all(&[comp_sep])?;
116 self.write_escaped(
117 std::str::from_utf8(part).map_err(|_| EdifactError::InvalidUtf8)?,
118 )?;
119 }
120 }
121 self.inner.write_all(&[self.ssa.segment_term])?;
122 self.segment_count += 1;
123 Ok(())
124 }
125
126 /// Write a segment from a tag and pre-split element/component data.
127 ///
128 /// `elements` is a slice of elements; each element is a sequence of component strings.
129 /// This avoids the lifetime constraints of [`Self::write_segment`] when building
130 /// segments from runtime-owned data (e.g. inside [`crate::WriterEmitter`]).
131 pub fn write_segment_parts<E>(&mut self, tag: &str, elements: &[E]) -> Result<(), EdifactError>
132 where
133 E: AsRef<[String]>,
134 {
135 self.inner.write_all(tag.as_bytes())?;
136 for element in elements {
137 self.inner.write_all(&[self.ssa.element_sep])?;
138 let mut first = true;
139 for comp in element.as_ref() {
140 if !first {
141 self.inner.write_all(&[self.ssa.component_sep])?;
142 }
143 first = false;
144 self.write_escaped(comp.as_str())?;
145 }
146 }
147 self.inner.write_all(&[self.ssa.segment_term])?;
148 self.segment_count += 1;
149 Ok(())
150 }
151
152 /// Flush and return the underlying writer.
153 pub fn finish(mut self) -> Result<W, EdifactError> {
154 self.inner.flush()?;
155 Ok(self.inner)
156 }
157
158 /// Write the `UNT` segment and return the inner writer.
159 ///
160 /// The segment count written into `UNT` element 1 (DE 0074) is the number of
161 /// segments already written **plus one** for the `UNT` segment itself, which
162 /// EDIFACT requires to be included in the count alongside `UNH`.
163 ///
164 /// # Errors
165 ///
166 /// Returns an error if writing fails. Do **not** call [`write_raw`][Self::write_raw] or
167 /// [`write_segment`][Self::write_segment] after `finish_unt` — the writer is consumed.
168 pub fn finish_unt(mut self, message_ref: &str) -> Result<W, EdifactError> {
169 // DE 0074: count includes UNH and UNT themselves.
170 let count = self.segment_count + 1;
171 let count_str = count.to_string();
172 self.write_raw("UNT", &[count_str.as_str(), message_ref])?;
173 self.finish()
174 }
175
176 /// Returns the total number of segments written so far.
177 pub fn segment_count(&self) -> u64 {
178 self.segment_count
179 }
180
181 /// Returns the active [`ServiceStringAdvice`] (delimiter configuration).
182 pub fn service_string_advice(&self) -> ServiceStringAdvice {
183 self.ssa
184 }
185
186 /// Escape a value string for inclusion in an EDIFACT segment.
187 ///
188 /// Any character in `value` that matches the active element separator,
189 /// component separator, release character, or segment terminator is escaped
190 /// by prefixing it with the release character (default `?`).
191 ///
192 /// Returns a borrowed `Cow::Borrowed(value)` when no escaping is needed,
193 /// avoiding an allocation on the fast path.
194 ///
195 /// # Example
196 ///
197 /// ```rust,ignore
198 /// let writer = Writer::new(std::io::sink());
199 /// // '+' must be escaped since it is the default element separator.
200 /// assert_eq!(writer.escape_value("price+tax"), "price?+tax");
201 /// ```
202 pub fn escape_value<'v>(&self, value: &'v str) -> Cow<'v, str> {
203 let (elem, comp, release, term) = (
204 self.ssa.element_sep,
205 self.ssa.component_sep,
206 self.ssa.release_char,
207 self.ssa.segment_term,
208 );
209 let bytes = value.as_bytes();
210 let needs_escape = bytes
211 .iter()
212 .any(|&b| b == elem || b == comp || b == release || b == term);
213 if !needs_escape {
214 return Cow::Borrowed(value);
215 }
216 let mut out = Vec::with_capacity(value.len() + 4);
217 let mut last = 0;
218 let mut pos = 0;
219 while pos < bytes.len() {
220 let remaining = &bytes[pos..];
221 let hit_ecr = memchr::memchr3(elem, comp, release, remaining);
222 let hit_t = memchr::memchr(term, remaining);
223 let hit = match (hit_ecr, hit_t) {
224 (None, None) => break,
225 (Some(a), None) => a,
226 (None, Some(b)) => b,
227 (Some(a), Some(b)) => a.min(b),
228 };
229 let abs = pos + hit;
230 out.extend_from_slice(&bytes[last..abs]);
231 out.push(release);
232 out.push(bytes[abs]);
233 last = abs + 1;
234 pos = abs + 1;
235 }
236 out.extend_from_slice(&bytes[last..]);
237 // SAFETY:
238 // 1. `value` is a valid `&str`, so `bytes` is valid UTF-8 to start.
239 // 2. `self.ssa.release_char` is a single-byte ASCII value (0x21–0x7E),
240 // enforced at construction time by `ServiceStringAdvice::is_valid()`
241 // (called in `Writer::with_una`; the default SSA hardcodes `?` = 0x3F).
242 // Inserting a single ASCII byte cannot split or corrupt a multi-byte
243 // UTF-8 sequence, because ASCII bytes always have the high bit clear
244 // while continuation bytes of multi-byte sequences always have the high
245 // bit set (0x80–0xBF).
246 // 3. All other bytes are copied verbatim from the valid UTF-8 source.
247 Cow::Owned(
248 String::from_utf8(out).expect(
249 "escape_value: output is not valid UTF-8; this is a bug in the escape logic",
250 ),
251 )
252 }
253 /// Write only the segment tag bytes — no element separator or terminator.
254 ///
255 /// Used by [`crate::WriterEmitter`] for eager, zero-allocation event writing.
256 #[inline]
257 pub(crate) fn write_tag_only(&mut self, tag: &str) -> Result<(), EdifactError> {
258 self.inner.write_all(tag.as_bytes())?;
259 Ok(())
260 }
261
262 /// Write one element separator byte.
263 #[inline]
264 pub(crate) fn write_element_sep(&mut self) -> Result<(), EdifactError> {
265 self.inner.write_all(&[self.ssa.element_sep])?;
266 Ok(())
267 }
268
269 /// Write one component separator byte.
270 #[inline]
271 pub(crate) fn write_component_sep(&mut self) -> Result<(), EdifactError> {
272 self.inner.write_all(&[self.ssa.component_sep])?;
273 Ok(())
274 }
275
276 /// Write the segment terminator and increment the internal segment counter.
277 #[inline]
278 pub(crate) fn write_segment_term_and_count(&mut self) -> Result<(), EdifactError> {
279 self.inner.write_all(&[self.ssa.segment_term])?;
280 self.segment_count += 1;
281 Ok(())
282 }
283
284 /// Write a value, escaping any delimiter characters.
285 pub(crate) fn write_escaped(&mut self, value: &str) -> Result<(), EdifactError> {
286 let (elem, comp, release, term) = (
287 self.ssa.element_sep,
288 self.ssa.component_sep,
289 self.ssa.release_char,
290 self.ssa.segment_term,
291 );
292 let bytes = value.as_bytes();
293 let mut last = 0;
294 let mut pos = 0;
295 while pos < bytes.len() {
296 // Use memchr3 for three delimiters + memchr for the fourth to avoid
297 // a manual byte-by-byte scan.
298 let remaining = &bytes[pos..];
299 let hit_ecr = memchr::memchr3(elem, comp, release, remaining);
300 let hit_t = memchr::memchr(term, remaining);
301 let hit = match (hit_ecr, hit_t) {
302 (None, None) => break,
303 (Some(a), None) => a,
304 (None, Some(b)) => b,
305 (Some(a), Some(b)) => a.min(b),
306 };
307 let abs = pos + hit;
308 if abs > last {
309 self.inner.write_all(&bytes[last..abs])?;
310 }
311 self.inner.write_all(&[release, bytes[abs]])?;
312 last = abs + 1;
313 pos = abs + 1;
314 }
315 self.inner.write_all(&bytes[last..])?;
316 Ok(())
317 }
318
319 // ── Interchange envelope helpers ──────────────────────────────────────────
320
321 /// Write a `UNB` interchange header segment.
322 ///
323 /// Generates:
324 /// ```text
325 /// UNB+<syntax_id>+<sender>+<recipient>+<datetime>+<control_ref>'
326 /// ```
327 ///
328 /// The caller is responsible for:
329 /// - Formatting `syntax_id` as a composite (e.g. `"UNOA:1"` for UN/EDIFACT syntax
330 /// version 1 of set A).
331 /// - Formatting `datetime` as a composite date-time (e.g. `"200101:0900"`).
332 ///
333 /// Track the `control_ref` — it must be repeated in the matching
334 /// [`end_interchange`](Self::end_interchange) call.
335 ///
336 /// # Errors
337 ///
338 /// Returns [`EdifactError`] if writing fails.
339 pub fn begin_interchange(
340 &mut self,
341 syntax_id: &str,
342 sender: &str,
343 recipient: &str,
344 datetime: &str,
345 control_ref: &str,
346 ) -> Result<(), EdifactError> {
347 self.write_raw(
348 "UNB",
349 &[syntax_id, sender, recipient, datetime, control_ref],
350 )
351 }
352
353 /// Write a `UNH` message header and return a [`MessageWriter`] guard.
354 ///
355 /// The guard tracks the per-message segment count automatically. Call
356 /// [`MessageWriter::finish`] when all message segments have been written — this
357 /// writes the matching `UNT` segment with the correct count. If `finish` is not
358 /// called, `Drop` will attempt to write `UNT` as a best-effort fallback (errors
359 /// are silently discarded on drop; prefer explicit `finish`).
360 ///
361 /// Generates:
362 /// ```text
363 /// UNH+<message_ref>+<message_type>:<version>:<release>:<controlling_agency>'
364 /// ```
365 ///
366 /// # Errors
367 ///
368 /// Returns [`EdifactError`] if writing the `UNH` segment fails.
369 pub fn begin_message<'w>(
370 &'w mut self,
371 message_ref: &str,
372 message_type: &str,
373 version: &str,
374 release: &str,
375 controlling_agency: &str,
376 ) -> Result<MessageWriter<'w, W>, EdifactError> {
377 let msg_id = format!("{message_type}:{version}:{release}:{controlling_agency}");
378 self.write_raw("UNH", &[message_ref, &msg_id])?;
379 // Capture `segment_count` after writing UNH so `MessageWriter` knows
380 // the absolute count that includes UNH.
381 let unh_count = self.segment_count;
382 Ok(MessageWriter {
383 writer: self,
384 message_ref: message_ref.to_owned(),
385 unh_count,
386 finished: false,
387 })
388 }
389
390 /// Write a `UNZ` interchange trailer segment.
391 ///
392 /// `message_count` is the number of `UNH`/`UNT` message pairs in the
393 /// interchange. `control_ref` must match the value passed to
394 /// [`begin_interchange`](Self::begin_interchange).
395 ///
396 /// If you used [`begin_message`](Self::begin_message) for every message in the
397 /// interchange, `message_count` equals the number of times you called that
398 /// method.
399 ///
400 /// # Errors
401 ///
402 /// Returns [`EdifactError`] if writing fails.
403 pub fn end_interchange(
404 &mut self,
405 message_count: u32,
406 control_ref: &str,
407 ) -> Result<(), EdifactError> {
408 let msg_count_str = message_count.to_string();
409 self.write_raw("UNZ", &[&msg_count_str, control_ref])
410 }
411}
412
413/// RAII guard for a single EDIFACT message within an interchange.
414///
415/// Obtained from [`Writer::begin_message`]. Writes `UNH` on creation and
416/// `UNT` (with the correct per-message segment count) when [`finish`](Self::finish)
417/// is called or the guard is dropped.
418///
419/// Always prefer calling [`finish`](Self::finish) explicitly so that write
420/// errors can be propagated. The `Drop` impl writes `UNT` as a best-effort
421/// fallback but silently discards I/O errors.
422///
423/// # Example
424///
425/// ```rust,no_run
426/// # use edifact_rs::{Writer, Segment};
427/// # fn example() -> Result<(), edifact_rs::EdifactError> {
428/// let mut writer = Writer::new(Vec::new());
429/// writer.begin_interchange("UNOA:1", "SENDER", "RECEIVER", "200101:0900", "1")?;
430/// {
431/// let mut msg = writer.begin_message("1", "ORDERS", "D", "96A", "UN")?;
432/// msg.write_raw("BGM", &["220", "PO001", "9"])?;
433/// msg.finish()?;
434/// }
435/// writer.end_interchange(1, "1")?;
436/// # Ok(())
437/// # }
438/// ```
439pub struct MessageWriter<'w, W: Write> {
440 writer: &'w mut Writer<W>,
441 message_ref: String,
442 /// Absolute segment count immediately after `UNH` was written.
443 unh_count: u64,
444 /// Set to `true` once `finish()` has been called to prevent a double-write
445 /// from the `Drop` impl.
446 finished: bool,
447}
448
449impl<'w, W: Write> MessageWriter<'w, W> {
450 /// Write a segment within this message.
451 ///
452 /// Delegates to [`Writer::write_raw`].
453 pub fn write_raw(&mut self, tag: &str, elements: &[&str]) -> Result<(), EdifactError> {
454 self.writer.write_raw(tag, elements)
455 }
456
457 /// Write a fully-typed segment within this message.
458 ///
459 /// Delegates to [`Writer::write_segment`].
460 pub fn write_segment(&mut self, seg: &Segment<'_>) -> Result<(), EdifactError> {
461 self.writer.write_segment(seg)
462 }
463
464 /// Compute the per-message segment count and write `UNT`, consuming the guard.
465 ///
466 /// The count written into `UNT` DE 0074 includes `UNH`, all content segments,
467 /// and `UNT` itself — matching the EDIFACT standard.
468 ///
469 /// # Errors
470 ///
471 /// Returns [`EdifactError`] if writing the `UNT` segment fails.
472 pub fn finish(mut self) -> Result<(), EdifactError> {
473 self.write_unt()?;
474 self.finished = true;
475 Ok(())
476 }
477
478 fn write_unt(&mut self) -> Result<(), EdifactError> {
479 // Segments since UNH: writer.segment_count - unh_count (content only).
480 // Total = 1 (UNH) + content + 1 (UNT) = content + 2.
481 let count = self.writer.segment_count - self.unh_count + 2;
482 let count_str = count.to_string();
483 self.writer
484 .write_raw("UNT", &[&count_str, &self.message_ref])
485 }
486}
487
488impl<'w, W: Write> Drop for MessageWriter<'w, W> {
489 fn drop(&mut self) {
490 if !self.finished {
491 // Best-effort: write UNT; errors cannot be propagated from drop.
492 let _ = self.write_unt();
493 }
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use crate::model::Element;
501
502 #[test]
503 fn write_and_parse_simple_segment() {
504 let segs: Vec<Segment<'static>> = vec![Segment::new(
505 "BGM",
506 vec![Element::of(&["220"]), Element::of(&["ORDER123"])],
507 )];
508 let bytes = crate::segments_to_bytes(&segs).unwrap();
509 let s = std::str::from_utf8(&bytes).unwrap();
510 assert!(s.starts_with("BGM+220+ORDER123'"));
511 }
512
513 #[test]
514 fn release_char_escaped() {
515 let segs: Vec<Segment<'static>> = vec![Segment::new(
516 "FTX",
517 vec![Element::of(&["value+with+delimiters"])],
518 )];
519 let bytes = crate::segments_to_bytes(&segs).unwrap();
520 let s = std::str::from_utf8(&bytes).unwrap();
521 // The `+` in the value must be escaped as `?+`
522 assert!(s.contains("?+"), "escape missing: {s}");
523 }
524
525 #[test]
526 fn round_trip_preserves_values() {
527 let segs: Vec<Segment<'static>> = vec![
528 Segment::new(
529 "UNB",
530 vec![
531 Element::of(&["UNOA", "1"]),
532 Element::of(&["SENDER"]),
533 Element::of(&["RECEIVER"]),
534 ],
535 ),
536 Segment::new("UNZ", vec![Element::of(&["0"]), Element::of(&["1"])]),
537 ];
538 let bytes = crate::segments_to_bytes(&segs).unwrap();
539 let rt: Vec<crate::OwnedSegment> = crate::parser::from_reader(std::io::Cursor::new(&bytes))
540 .expect("round-trip parse failed");
541 assert_eq!(rt[0].tag, "UNB");
542 assert_eq!(rt[0].as_borrowed().element_str(0), Some("UNOA"));
543 assert_eq!(rt[1].tag, "UNZ");
544 }
545
546 /// Verify that `Writer::with_una` uses the configured delimiters throughout,
547 /// and that `write_segment_parts` (the delimiter-agnostic API) produces correct
548 /// component separators even with a non-default UNA.
549 #[test]
550 fn with_una_non_default_delimiters() {
551 use crate::tokenizer::ServiceStringAdvice;
552
553 // Custom UNA: comp_sep=| elem_sep=! esc=? dec_mark=, rep_sep=* seg_term=~
554 let ssa = ServiceStringAdvice {
555 component_sep: b'|',
556 element_sep: b'!',
557 release_char: b'?',
558 decimal_mark: b',',
559 repetition_sep: b'*',
560 segment_term: b'~',
561 };
562
563 let buf = Vec::new();
564 let mut writer = Writer::with_una(buf, ssa).expect("writer creation failed");
565
566 // write_segment_parts: pre-split; no hard-coded `:` in element strings
567 writer
568 .write_segment_parts(
569 "BGM",
570 &[
571 vec!["220".to_owned(), "SUB1".to_owned()],
572 vec!["PO1".to_owned()],
573 ],
574 )
575 .expect("write failed");
576
577 let out = writer.finish().expect("finish failed");
578 let s = std::str::from_utf8(&out).unwrap();
579
580 // Output must use `!` as element separator, `|` as component separator, `~` as terminator.
581 // The writer also emits a UNA header when with_una is used.
582 assert!(s.contains("BGM"), "BGM segment missing: {s}");
583 // Slice after UNA so assertions target segment output, not UNA header bytes.
584 let after_una = s.find("BGM").map(|i| &s[i..]).unwrap_or(s);
585 assert!(
586 after_una.contains('!'),
587 "missing element sep in segment: {after_una}"
588 );
589 assert!(
590 after_una.contains('|'),
591 "missing component sep in segment: {after_una}"
592 );
593 assert!(
594 after_una.ends_with('~'),
595 "missing segment term in segment: {after_una}"
596 );
597 // Decimal mark appears in the UNA header (no decimal-bearing values in this segment).
598 assert!(s.contains(','), "missing decimal mark in UNA: {s}");
599 assert!(!s.contains('+'), "default element sep leaked: {s}");
600 assert!(!s.contains(':'), "default component sep leaked: {s}");
601 // segment_term '~' is not the default; ensure no default ' leaks (UNA itself aside)
602 assert!(
603 !after_una.contains('\''),
604 "default segment term leaked after UNA: {after_una}"
605 );
606 }
607}