edifact_rs/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![deny(unsafe_code)]
3
4//! `edifact-rs` — zero-copy EDIFACT tokenizer, parser, writer, serde traits,
5//! validation engine, and extensible directory support.
6//!
7//! `edifact-rs` is the main entry point of this workspace. The core parsing,
8//! writing, and validation infrastructure is always available. Custom directory
9//! validators can be implemented by downstream crates or generated through
10//! external build tooling.
11//!
12//! # Quick start
13//! ```
14//! use edifact_rs::from_bytes;
15//! let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
16//! let segments: Vec<_> = from_bytes(input).collect::<Result<_, _>>().unwrap();
17//! assert_eq!(segments[0].tag, "UNB");
18//! ```
19//!
20//! # Crate features
21//!
22//! - `derive` (enabled by default): re-exports the derive macros from
23//! `edifact-rs-derive` — [`EdifactDeserialize`][macro@EdifactDeserialize] /
24//! [`EdifactSerialize`][macro@EdifactSerialize] for segment and message
25//! structs, and
26//! [`EdifactCompositeDeserialize`][macro@EdifactCompositeDeserialize] /
27//! [`EdifactCompositeSerialize`][macro@EdifactCompositeSerialize] for the
28//! composite-element structs they reference.
29//! - `diagnostics` (disabled by default): enables rich diagnostic output via `miette`.
30//! When enabled, errors implement `miette::Diagnostic` for enhanced error reporting.
31//! This feature adds an optional dependency and has no impact on parsing performance.
32//! - `serde` (disabled by default): derives `Serialize` / `Deserialize` for
33//! [`ValidationReport`], [`ValidationIssue`], and the envelope types, so
34//! reports can be persisted or sent across a queue and read back.
35//!
36//! Features are additive and independent: enabling any combination changes only
37//! which trait impls and re-exports are available, never parsing or validation
38//! behaviour.
39//!
40//! The crate is expected to compile both with defaults and with
41//! `--no-default-features` for consumers who only want the core parsing and
42//! writing functionality.
43//!
44//! ## Feature matrix workflows
45//!
46//! - default features:
47//! `cargo test -p edifact-rs`
48//! - no default features:
49//! `cargo test -p edifact-rs --no-default-features`
50//! - all features:
51//! `cargo test -p edifact-rs --all-features`
52//!
53//! # Diagnostic Feature
54//!
55//! When the `diagnostics` feature is enabled, [`EdifactError`] gains additional
56//! traits and methods that enable rich, human-readable error output:
57//!
58//! ```text
59//! Error: invalid delimiter byte 0xAB at offset 42
60//!
61//! ╭─ input.edi:2:3
62//! │
63//! 2 │ UNB+UNOA:1+....[invalid]...
64//! │ ^^^ invalid byte here
65//! │
66//! Error Code: E002
67//! Help: The byte 0xAB is not a valid delimiter. Check UNA configuration
68//! ```
69//!
70//! This feature is useful for CLI tools and error reporting, but is not required
71//! for applications that handle errors programmatically.
72//!
73//! # Parse And Text Contracts
74//!
75//! Parsing in `edifact-rs` is strict and deterministic:
76//!
77//! - Segment and element text must decode as UTF-8 (`E003` on failure).
78//! - Release characters must escape exactly one following byte.
79//! A trailing `?` at end-of-input is rejected (`E019`).
80//! - Malformed delimiters and truncated segments are reported with stable
81//! error codes rather than panicking.
82//! - Every [`ReaderConfig`] budget is a hard cap that **reports** a violation
83//! (`E020` for the per-segment size guard, `E036` for the whole-input
84//! budgets). A limit never ends the iterator quietly, because that is
85//! indistinguishable from a clean end of input and would let a caller accept
86//! a truncated interchange as a complete one.
87//! - The service characters are discovered the way ISO 9735-1 says a receiver
88//! should discover them: from a leading `UNA` if there is one, otherwise the
89//! §5.1 defaults with the repetition separator resolved from the syntax
90//! version in `UNB` S001 DE 0002 — active as `*` for version 4, inactive for
91//! versions 1–3, where `*` is ordinary data. Override both with
92//! [`ReaderConfig::with_service_string_advice`] when parsing a fragment that
93//! carries neither header.
94//! - When the repetition separator is active, repeating data elements are split
95//! into [`Element::repetitions`] rather than left glued into the value
96//! (ISO 9735-1 §8.6).
97//!
98//! These contracts apply to both slice-based parsing (`from_bytes`) and
99//! reader-based parsing (`from_reader`).
100//!
101//! ```
102//! use edifact_rs::from_reader_collect;
103//! use std::io::Cursor;
104//!
105//! let input = b"UNA:;.? 'BGM;220;test?;value'";
106//! let segments = from_reader_collect(Cursor::new(&input[..])).unwrap();
107//! assert_eq!(segments.len(), 1);
108//! assert_eq!(segments[0].tag, "BGM");
109//! assert_eq!(segments[0].element_str(0), Some("220"));
110//! assert_eq!(segments[0].element_str(1), Some("test;value"));
111//! ```
112//!
113//! # Validation Quick Start
114//!
115//! The `Validator` trait and `ValidationContext` provide a flexible framework
116//! for building custom validators. Users can generate validators from official
117//! UNECE sources or implement their own.
118//!
119//! See the [`Validator`] trait documentation and the `cookbook_fixture_validation.rs`
120//! example for details on creating custom validators.
121//!
122//! # Custom Profile Packs
123//!
124//! `ProfileRulePack` is the extension point for downstream MIG/profile crates.
125//! Packs can be authored with public APIs only and plugged into a
126//! [`ValidationContext`]:
127//!
128//! ```
129//! use edifact_rs::{
130//! from_bytes, ProfileRulePack, ValidationContext, ValidationIssue, ValidationSeverity,
131//! };
132//!
133//! let segments: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'")
134//! .collect::<Result<_, _>>()?;
135//!
136//! let pack = ProfileRulePack::new("ORDERS-DEMO")
137//! .for_message_type("ORDERS")
138//! .with_stateless_rule_fn(|segments, issues| {
139//! if let Some(bgm) = segments.iter().find(|segment| segment.tag == "BGM") {
140//! if let Some(code) = bgm.get_element(0).and_then(|e| e.get_component(0)) {
141//! if code == "220" {
142//! issues.push(
143//! ValidationIssue::new(
144//! ValidationSeverity::Warning,
145//! "demo pack rejects BGM 220 for illustration",
146//! )
147//! .with_rule_id("DEMO-P001")
148//! .with_segment("BGM")
149//! .with_element_index(0),
150//! );
151//! }
152//! }
153//! }
154//! });
155//!
156//! let report = ValidationContext::builder()
157//! .with_profile_pack(pack)
158//! .build()
159//! .validate_lenient(&segments);
160//!
161//! assert!(report.has_warnings());
162//! let partner_report = report.filter_by_rule_prefix("DEMO-");
163//! assert!(partner_report.total_issues() >= 1);
164//! # Ok::<(), edifact_rs::EdifactError>(())
165//! ```
166//!
167//! # Async Usage
168//!
169//! `edifact-rs` does not provide a native `async` API. All parsing is
170//! synchronous and driven by the standard `std::io::Read` / `std::io::BufRead`
171//! traits. The recommended integration pattern with async runtimes is:
172//!
173//! 1. Use your async runtime's read utilities to read the entire message into a
174//! `Vec<u8>` (e.g. `tokio::io::AsyncReadExt::read_to_end`).
175//! 2. Parse the in-memory slice with [`from_bytes`].
176//!
177//! ```rust,no_run
178//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
179//! // With tokio:
180//! // let mut buf = Vec::new();
181//! // reader.read_to_end(&mut buf).await?;
182//! // let segments: Vec<_> = edifact_rs::from_bytes(&buf).collect::<Result<_, _>>()?;
183//! # Ok(())
184//! # }
185//! ```
186// ── core modules ──────────────────────────────────────────────────────────────
187/// EDIFACT character repertoires (`UNB` S001 DE 0001) and transcoding.
188pub mod charset;
189/// `CONTRL` — the ISO 9735-4 syntax and service report message.
190pub mod contrl;
191pub mod directory_validator;
192pub(crate) mod envelope;
193/// Error types and validation reporting primitives.
194pub(crate) mod error;
195pub mod group;
196/// Core zero-copy and owned EDIFACT data model types.
197pub(crate) mod model;
198pub(crate) mod parser;
199/// Validation report types: [`ValidationSeverity`], [`ValidationIssue`], [`ValidationReport`].
200///
201/// These types are also re-exported from the crate root.
202pub mod report;
203/// ISO 9735 service-segment definitions (`UNB`, `UNH`, `UNT`, `UNZ`, `UNG`, `UNE`, `UNS`).
204pub mod service;
205pub(crate) mod tokenizer;
206pub(crate) mod validator;
207pub(crate) mod writer;
208
209// ── typed serialization layer ─────────────────────────────────────────────────
210pub mod de;
211pub(crate) mod event;
212pub mod ser;
213
214// ── flat re-exports: core ─────────────────────────────────────────────────────
215pub use charset::{Charset, DecodingReader, decode_interchange, decode_reader, sniff_charset};
216pub use contrl::{Action, Contrl, ReportingLevel, SyntaxError};
217pub use envelope::{
218 FunctionalGroupEnvelope, GroupIdentifier, InterchangeEnvelope, LenientResult, MessageEnvelope,
219 MessageIdentifier, ValidatedInterchange, parse_ung, parse_unh, validate_envelope,
220 validate_envelope_from_owned, validate_envelope_lenient, validate_envelope_lenient_from_owned,
221};
222pub use error::{EdifactError, Insignificant, IoError};
223pub use group::{
224 GroupDef, SegmentGroupIndexed, group_owned_segments_indexed, group_segments_indexed,
225};
226pub use model::{
227 BorrowedElement, BorrowedSegment, Components, Element, OwnedComponents, OwnedElement,
228 OwnedSegment, Segment, Span,
229};
230pub use parser::{
231 OwnedSegmentStream, Parser, ReaderConfig, from_bufread, from_bufread_stream,
232 from_bufread_stream_with_config, from_reader_with_config,
233};
234pub use report::severity_for_error;
235pub use report::{ValidationIssue, ValidationReport, ValidationSeverity};
236pub use tokenizer::{ServiceStringAdvice, Token, Tokenizer};
237pub use validator::{
238 CharsetValidator, EnvelopeValidator, ProfileRule, ProfileRulePack, SyntaxValidator,
239 ValidationContext, ValidationContextBuilder, ValidationLayer, ValidationRuleContext, Validator,
240 validate_each,
241};
242pub use writer::{AsDataElement, DataElement, MessageWriter, Writer};
243
244// ── flat re-exports: serde ────────────────────────────────────────────────────
245
246/// User-facing deserialization API.
247pub use de::{
248 CompositeElement, DispatchedMessage, EdifactCompositeDeserialize, EdifactDeserialize,
249 EdifactSegmentTag, MessageDispatch, MessageWindow, MessageWindowsIter, MessageWindowsSliceIter,
250 OwnedMessageWindow, SegmentAccessor, composite_element, contiguous_groups_by_qualifier,
251 contiguous_groups_iter, deserialize, deserialize_all_from_reader, deserialize_all_streaming,
252 deserialize_first_from_reader, deserialize_first_streaming, deserialize_messages_bytes,
253 deserialize_messages_from_reader, deserialize_str, element_str, find_qualified_segment,
254 find_qualified_segment_owned, find_segment, find_segment_owned, find_segment_typed,
255 find_segments_iter, find_segments_typed, get_components_iter,
256 groups_are_contiguous_by_qualifier, message_windows_from_reader, optional_component,
257 optional_element, qualifier_matches_pattern, repeated_components, repeated_components_owned,
258 required_component, required_element,
259};
260
261/// Splits a byte slice into [`MessageWindow`] views, one per `UNH`/`UNT` envelope,
262/// enabling parallel or lazy per-message processing without copying data.
263///
264/// # Example
265/// ```rust
266/// use edifact_rs::from_bytes_windows;
267///
268/// let input = b"UNB+UNOA:1+S+R+200101:0900+1'\
269/// UNH+1+ORDERS:D:96A:UN'BGM+220+A+9'UNT+3+1'\
270/// UNH+2+ORDERS:D:96A:UN'BGM+220+B+9'UNT+3+2'\
271/// UNZ+2+1'";
272/// let windows: Vec<_> = from_bytes_windows(input).collect::<Result<Vec<_>, _>>()?;
273///
274/// assert_eq!(windows.len(), 2);
275/// // Each window spans exactly one UNH..UNT pair.
276/// assert_eq!(windows[0].segments.first().unwrap().tag, "UNH");
277/// assert_eq!(windows[0].segments.last().unwrap().tag, "UNT");
278/// # Ok::<(), edifact_rs::EdifactError>(())
279/// ```
280pub use de::message_windows_bytes as from_bytes_windows;
281
282// ── Proc-macro support ─────────────────────────────────────────────────────────
283
284pub use directory_validator::{
285 ComponentRef, DirectoryValidator, DirectoryValidatorBuilder, ElementPath, ElementRef,
286 LayoutAudit, LayoutFinding, LayoutSlot, OwnedComponentRef, OwnedElementRef, OwnedSegmentDef,
287 Repr, ReprKind, SegmentDefinition, SegmentLayout, Status, audit_directory,
288};
289#[cfg(feature = "derive")]
290#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
291pub use edifact_rs_derive::{
292 EdifactCompositeDeserialize, EdifactCompositeSerialize, EdifactDeserialize, EdifactSerialize,
293};
294pub use event::{EdifactEvent, EventEmitter, OwnedEdifactEvent, VecEmitter, WriterEmitter};
295pub use ser::{
296 DecimalFloat, DecimalFloatDisplay, EdifactCompositeSerialize, EdifactSerialize,
297 emit_sparse_segment, to_bytes, to_edifact_string,
298};
299
300// ── core free functions ───────────────────────────────────────────────────────
301
302use std::io::{Read, Write};
303
304/// Iterator returned by [`from_bytes`].
305pub struct FromBytesIter<'a> {
306 parser: Option<parser::Parser<'a>>,
307 pending_error: Option<EdifactError>,
308 config: ReaderConfig,
309 /// Segments successfully yielded so far.
310 segments_yielded: usize,
311 /// Complete `UNH`/`UNT` message pairs yielded so far.
312 messages_yielded: usize,
313 /// Whether a `UNH` has been yielded without its matching `UNT`.
314 in_message: bool,
315}
316
317/// Iterator returned by [`from_reader`].
318pub struct FromReaderIter<R: Read> {
319 inner: parser::OwnedSegmentStream<std::io::BufReader<R>>,
320}
321
322impl<R: Read> Iterator for FromReaderIter<R> {
323 type Item = Result<OwnedSegment, EdifactError>;
324
325 fn next(&mut self) -> Option<Self::Item> {
326 self.inner.next()
327 }
328}
329
330impl<'a> Iterator for FromBytesIter<'a> {
331 type Item = Result<Segment<'a>, EdifactError>;
332
333 fn next(&mut self) -> Option<Self::Item> {
334 if let Some(err) = self.pending_error.take() {
335 self.parser = None;
336 return Some(Err(err));
337 }
338 // Limits are checked against a segment that is actually available, so an
339 // input ending exactly at the limit finishes cleanly instead of being
340 // reported as a violation.
341 let seg = match self.parser.as_mut()?.next()? {
342 Ok(seg) => seg,
343 Err(error) => {
344 self.parser = None;
345 return Some(Err(error));
346 }
347 };
348
349 if let Some(max) = self.config.max_segments {
350 if self.segments_yielded >= max {
351 return Some(Err(self.exceeded("max_segments", max as u64)));
352 }
353 }
354 // Only a `UNH` opens a message, so only a `UNH` can push the count past
355 // the budget. Testing every segment would trip on the interchange
356 // trailer, which belongs to no message.
357 if let Some(max) = self.config.max_messages {
358 if seg.tag == "UNH" && self.messages_yielded >= max {
359 return Some(Err(self.exceeded("max_messages", max as u64)));
360 }
361 }
362 // `seg.span.end` is the byte offset just past this segment's terminator —
363 // an absolute cursor that already accounts for the UNA header, the
364 // separators, and the terminator itself.
365 if let Some(max) = self.config.max_input_bytes {
366 if seg.span.end as u64 > max {
367 return Some(Err(self.exceeded("max_input_bytes", max)));
368 }
369 }
370
371 self.segments_yielded += 1;
372 if seg.tag == "UNT" {
373 if self.in_message {
374 self.messages_yielded += 1;
375 }
376 self.in_message = false;
377 } else if seg.tag == "UNH" {
378 self.in_message = true;
379 }
380 Some(Ok(seg))
381 }
382}
383
384impl FromBytesIter<'_> {
385 /// Terminate the iterator and report the limit that tripped.
386 #[inline]
387 fn exceeded(&mut self, limit: &'static str, max: u64) -> EdifactError {
388 self.parser = None;
389 EdifactError::LimitExceeded { limit, max }
390 }
391}
392
393/// Parse `input` bytes into an iterator of [`Segment`]s.
394///
395/// Borrows directly from `input` — zero allocation for segment data.
396///
397/// # Segment-size limit
398///
399/// Applies a default 64 KiB per-segment limit, matching the reader-based path.
400/// Use [`from_bytes_with_config`] to override.
401pub fn from_bytes(input: &[u8]) -> FromBytesIter<'_> {
402 from_bytes_with_config(input, parser::ReaderConfig::default())
403}
404
405/// Parse `input` bytes into an iterator of [`Segment`]s with explicit configuration.
406///
407/// Every [`ReaderConfig`] limit is enforced as a **hard cap that yields an error**,
408/// never as a silent stop:
409///
410/// - `max_segment_bytes` — [`EdifactError::SegmentTooLong`] when a single segment
411/// exceeds the threshold.
412/// - `max_segments`, `max_messages`, `max_input_bytes` —
413/// [`EdifactError::LimitExceeded`] when the input carries more than the budget.
414///
415/// A budget that merely ended the iterator would be indistinguishable from a clean
416/// end of input, so a caller collecting into a `Vec` would silently accept a
417/// **truncated** interchange as a complete one. Input that ends exactly at a limit
418/// is not a violation and finishes normally.
419///
420/// Pass `ReaderConfig::default()` for the default 64 KiB per-segment limit with no
421/// segment-count, message-count, or byte budget.
422///
423/// # Example
424///
425/// ```
426/// use edifact_rs::{EdifactError, ReaderConfig, from_bytes_with_config};
427///
428/// // Exactly at the limit: fine.
429/// let cfg = ReaderConfig::default().max_segments(1);
430/// assert!(from_bytes_with_config(b"BGM+220'", cfg).collect::<Result<Vec<_>, _>>().is_ok());
431///
432/// // One segment too many: a loud error, not a quiet truncation.
433/// let err = from_bytes_with_config(b"BGM+220'DTM+137'", cfg)
434/// .collect::<Result<Vec<_>, _>>()
435/// .unwrap_err();
436/// assert!(matches!(err, EdifactError::LimitExceeded { limit: "max_segments", max: 1 }));
437/// ```
438pub fn from_bytes_with_config(input: &[u8], config: parser::ReaderConfig) -> FromBytesIter<'_> {
439 // A malformed `UNA` is rejected even when the caller supplied its own
440 // service characters: the input is broken either way, and silently parsing
441 // past a nine-byte header nobody validated would be the worse answer.
442 let discovered = tokenizer::ServiceStringAdvice::from_bytes(input);
443 let resolved = match (config.service_string_advice, discovered) {
444 (_, Err(error)) => Err(error),
445 (Some(override_ssa), Ok(_)) => Ok(override_ssa),
446 (None, Ok(ssa)) => Ok(ssa),
447 };
448 let (parser, pending_error) = match resolved {
449 Ok(ssa) => {
450 let t = tokenizer::Tokenizer::with_limit(input, ssa, config.max_segment_bytes);
451 (Some(parser::Parser::new(t)), None)
452 }
453 Err(error) => (None, Some(error)),
454 };
455 FromBytesIter {
456 parser,
457 pending_error,
458 config,
459 segments_yielded: 0,
460 messages_yielded: 0,
461 in_message: false,
462 }
463}
464
465/// Parse a reader into a lazy iterator of [`OwnedSegment`]s.
466///
467/// Returns a [`FromReaderIter`] that parses and yields segments on demand,
468/// keeping memory bounded. Use [`from_reader_collect`] to eagerly materialise
469/// all segments into a `Vec`.
470///
471/// # Errors
472///
473/// Each `next()` call yields `Some(Ok(segment))` for a successfully parsed
474/// segment, `Some(Err(EdifactError))` for a parse or I/O failure, and `None`
475/// when the end of the stream has been reached.
476pub fn from_reader<R: Read>(reader: R) -> FromReaderIter<R> {
477 FromReaderIter {
478 inner: parser::from_reader_stream(reader),
479 }
480}
481
482/// Parse a reader into an owned `Vec` of all segments.
483///
484/// Eagerly collects the full interchange into memory. If you only need a
485/// subset of segments, prefer [`from_reader`] (lazy iterator) to avoid
486/// unnecessary allocations.
487///
488/// # Errors
489///
490/// Returns an error if the input contains malformed EDIFACT syntax,
491/// invalid UTF-8 segment text, dangling release sequences, or underlying I/O failures.
492pub fn from_reader_collect<R: Read>(reader: R) -> Result<Vec<OwnedSegment>, EdifactError> {
493 parser::from_reader(reader)
494}
495
496/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s.
497///
498/// Unlike [`from_bytes`] (which yields borrowed [`Segment`]s tied to the input
499/// lifetime), every segment returned here is fully owned. This is convenient
500/// when you need to store or return segments without retaining a reference to
501/// the original byte slice.
502///
503/// # Example
504///
505/// ```
506/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned(b"BGM+220+1+9'")
507/// .collect::<Result<_, _>>()
508/// .unwrap();
509/// assert_eq!(segs[0].tag, "BGM");
510/// ```
511pub fn from_bytes_owned(
512 input: &[u8],
513) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
514 from_bytes(input).map(|r| r.map(OwnedSegment::from))
515}
516
517/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s with a
518/// custom [`ReaderConfig`].
519///
520/// Identical to [`from_bytes_owned`] but applies the limits and settings from
521/// `config` (e.g. `max_segment_bytes`, `max_segments`, `max_input_bytes`).
522///
523/// # Example
524///
525/// ```
526/// use edifact_rs::ReaderConfig;
527/// let config = ReaderConfig::default().max_segments(10);
528/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned_with_config(
529/// b"BGM+220+1+9'",
530/// config,
531/// )
532/// .collect::<Result<_, _>>()
533/// .unwrap();
534/// assert_eq!(segs[0].tag, "BGM");
535/// ```
536pub fn from_bytes_owned_with_config(
537 input: &[u8],
538 config: ReaderConfig,
539) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
540 from_bytes_with_config(input, config).map(|r| r.map(OwnedSegment::from))
541}
542
543/// Parse a byte slice, decoding it from the repertoire its own `UNB` declares.
544///
545/// [`decode_interchange`] followed by [`from_bytes_owned`], in one call that a
546/// caller cannot forget to make. Forgetting is the failure mode worth designing
547/// against: a `UNOC` corpus stored as UTF-8 parses fine, so the tests pass and
548/// the first *conformant* counterparty message — the one with `ü` as the single
549/// byte `0xFC` — is rejected as invalid text.
550///
551/// Segments are owned because the decoded buffer is this function's, not the
552/// caller's: an ISO 8859-1 payload has to be transcoded to exist as UTF-8 at
553/// all. When the payload is already ASCII or `UNOY`, decoding borrows and copies
554/// nothing, but the segments are still owned — reach for
555/// [`decode_interchange`] plus [`from_bytes`] when you want to keep the
556/// zero-copy path and hold the buffer yourself.
557///
558/// # Errors
559///
560/// As [`decode_interchange`], plus any parse error.
561///
562/// # Example
563///
564/// ```
565/// // A conformant UNOC interchange: `Müller` is `4D FC 6C 6C 65 72`.
566/// let mut raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'NAD+BY+M".to_vec();
567/// raw.push(0xFC);
568/// raw.extend_from_slice(b"ller'UNZ+0+IC1'");
569///
570/// // Parsing it directly fails — it is not UTF-8, and it never claimed to be.
571/// assert!(edifact_rs::from_bytes(&raw).collect::<Result<Vec<_>, _>>().is_err());
572///
573/// let segments = edifact_rs::from_bytes_decoded(&raw)?;
574/// assert_eq!(segments[1].element_str(1), Some("Müller"));
575/// # Ok::<(), edifact_rs::EdifactError>(())
576/// ```
577pub fn from_bytes_decoded(input: &[u8]) -> Result<Vec<OwnedSegment>, EdifactError> {
578 from_bytes_decoded_with_config(input, ReaderConfig::default())
579}
580
581/// [`from_bytes_decoded`] with explicit [`ReaderConfig`] limits.
582///
583/// # Errors
584///
585/// As [`from_bytes_decoded`].
586pub fn from_bytes_decoded_with_config(
587 input: &[u8],
588 config: ReaderConfig,
589) -> Result<Vec<OwnedSegment>, EdifactError> {
590 let decoded = charset::decode_interchange(input)?;
591 from_bytes_owned_with_config(&decoded, config).collect()
592}
593
594/// Parse a reader, decoding it from the repertoire the stream's own `UNB`
595/// declares — **lazily**.
596///
597/// [`decode_reader`] has to read far enough to find the `UNB` before it can
598/// answer, so it returns a `Result` — and a `?` on it turns a lazy pipeline
599/// eager, forcing the caller to box the iterator or wrap the error in a
600/// one-item chain. This does the sniff on the first `next()` instead, so the
601/// signature stays a plain `Iterator` and a decode failure arrives as its first
602/// item, exactly like a parse failure does.
603///
604/// # Example
605///
606/// ```
607/// let mut raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'NAD+BY+M".to_vec();
608/// raw.push(0xFC);
609/// raw.extend_from_slice(b"ller'UNZ+0+IC1'");
610///
611/// // No `?` before the loop: the pipeline stays lazy.
612/// let segments: Vec<_> = edifact_rs::from_reader_decoded(std::io::Cursor::new(raw))
613/// .collect::<Result<Vec<_>, _>>()?;
614/// assert_eq!(segments[1].element_str(1), Some("Müller"));
615/// # Ok::<(), edifact_rs::EdifactError>(())
616/// ```
617pub fn from_reader_decoded<R: Read>(reader: R) -> DecodingSegmentStream<R> {
618 from_reader_decoded_with_config(reader, ReaderConfig::default())
619}
620
621/// [`from_reader_decoded`] with explicit [`ReaderConfig`] limits.
622pub fn from_reader_decoded_with_config<R: Read>(
623 reader: R,
624 config: ReaderConfig,
625) -> DecodingSegmentStream<R> {
626 DecodingSegmentStream {
627 state: DecodingState::Pending(reader),
628 config,
629 }
630}
631
632/// Lazy iterator returned by [`from_reader_decoded`].
633///
634/// Sniffs the interchange's repertoire on the first `next()`, so constructing it
635/// cannot fail and the caller keeps a plain `Iterator`.
636pub struct DecodingSegmentStream<R: Read> {
637 state: DecodingState<R>,
638 config: ReaderConfig,
639}
640
641type DecodedReader<R> = charset::DecodingReader<std::io::Chain<std::io::Cursor<Vec<u8>>, R>>;
642
643enum DecodingState<R: Read> {
644 /// Nothing read yet; the repertoire is still unknown.
645 Pending(R),
646 /// Repertoire resolved; segments are streaming.
647 Running(Box<parser::OwnedSegmentStream<std::io::BufReader<DecodedReader<R>>>>),
648 /// Terminated, by exhaustion or by a decode failure already reported.
649 Done,
650}
651
652impl<R: Read> Iterator for DecodingSegmentStream<R> {
653 type Item = Result<OwnedSegment, EdifactError>;
654
655 fn next(&mut self) -> Option<Self::Item> {
656 loop {
657 match &mut self.state {
658 DecodingState::Done => return None,
659 DecodingState::Running(stream) => return stream.next(),
660 DecodingState::Pending(_) => {
661 let DecodingState::Pending(reader) =
662 std::mem::replace(&mut self.state, DecodingState::Done)
663 else {
664 unreachable!("guarded by the match arm")
665 };
666 // The sniff happens here rather than at construction, which
667 // is what keeps the signature a plain `Iterator`.
668 match charset::decode_reader(reader) {
669 Ok(decoded) => {
670 self.state = DecodingState::Running(Box::new(
671 parser::from_reader_with_config(decoded, self.config),
672 ));
673 }
674 Err(error) => return Some(Err(error)),
675 }
676 }
677 }
678 }
679 }
680}
681
682/// Serialize `segments` to an [`std::io::Write`] implementation.
683///
684/// # Errors
685///
686/// Returns an error if writing fails or if segment serialization fails.
687pub fn to_writer<'a, 'b, W, I>(w: W, segments: I) -> Result<(), EdifactError>
688where
689 'b: 'a,
690 W: Write,
691 I: IntoIterator<Item = &'a Segment<'b>>,
692{
693 let mut wr = writer::Writer::new(w);
694 for seg in segments {
695 wr.write_segment(seg)?;
696 }
697 wr.finish().map(|_| ())
698}
699
700/// Serialize `segments` to an owned `Vec<u8>`.
701///
702/// # Errors
703///
704/// Returns an error if serialization fails.
705pub fn segments_to_bytes<'a, 'b, I>(segments: I) -> Result<Vec<u8>, EdifactError>
706where
707 'b: 'a,
708 I: IntoIterator<Item = &'a Segment<'b>>,
709{
710 let mut buf = Vec::new();
711 to_writer(&mut buf, segments)?;
712 Ok(buf)
713}
714
715/// Serialize a slice of [`OwnedSegment`]s to an owned `Vec<u8>`.
716///
717/// Convenience wrapper around [`to_writer`] that accepts owned segments
718/// directly. Each segment is converted to its borrowed form on the fly
719/// and written immediately — no intermediate `Vec<Segment<'_>>` is
720/// allocated, so peak memory stays proportional to one segment at a time
721/// rather than the full slice.
722///
723/// # Errors
724///
725/// Returns an error if serialization fails.
726pub fn segments_to_bytes_owned(segments: &[OwnedSegment]) -> Result<Vec<u8>, EdifactError> {
727 let mut buf = Vec::new();
728 let mut wr = writer::Writer::new(&mut buf);
729 for seg in segments {
730 wr.write_segment(&seg.as_borrowed())?;
731 }
732 wr.finish()?;
733 Ok(buf)
734}
735
736/// Validate the envelope structure of an owned-segment slice.
737///
738/// Convenience wrapper that accepts `&[OwnedSegment]` without requiring a
739/// manual conversion to borrowed segments. Unlike the previous implementation,
740/// no intermediate `Vec<Segment<'_>>` is allocated — segments are read directly.
741///
742/// # Errors
743///
744/// Returns an error if the envelope is structurally invalid.
745pub fn validate_envelope_owned(
746 segments: &[OwnedSegment],
747) -> Result<ValidatedInterchange, EdifactError> {
748 envelope::validate_envelope_from_owned(segments)
749}
750
751/// Lenient envelope validation over owned segments — collects all errors.
752///
753/// Convenience wrapper around [`validate_envelope_lenient_from_owned`].
754/// Returns a [`LenientResult`] with `Some(result)` and empty errors on success.
755/// On count-only violations, returns `Some(partial)` with errors.
756/// On structural failures, returns `None` with errors.
757pub fn validate_envelope_lenient_owned(segments: &[OwnedSegment]) -> LenientResult {
758 envelope::validate_envelope_lenient_from_owned(segments)
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 #[test]
766 fn from_bytes_rejects_invalid_una() {
767 let err = from_bytes(b"UNA::.? 'BGM:220'")
768 .collect::<Result<Vec<_>, _>>()
769 .expect_err("invalid UNA should fail slice parsing");
770 assert!(matches!(err, EdifactError::InvalidUna));
771 }
772}
773
774/// Compiles and runs every ```` ```rust ```` block in the published guides as a
775/// doctest.
776///
777/// The guides drifted from the API — snippets referenced private module paths
778/// and methods that did not exist — because nothing ever compiled them. Wiring
779/// them in here means a rename that breaks a guide breaks the build.
780///
781/// Blocks that genuinely cannot run (they need a live socket, a real directory
782/// file, or a downstream crate) should be marked ```` ```rust,ignore ```` or
783/// ```` ```rust,no_run ```` in the guide itself.
784#[cfg(doctest)]
785mod doc_guides {
786 macro_rules! guide {
787 ($name:ident, $path:literal) => {
788 #[doc = include_str!($path)]
789 pub struct $name;
790 };
791 }
792
793 guide!(
794 CharacterSets,
795 "../../../site/content/docs/character-sets.md"
796 );
797 guide!(Contrl, "../../../site/content/docs/contrl.md");
798 guide!(CoreConcepts, "../../../site/content/docs/core-concepts.md");
799 guide!(Parsing, "../../../site/content/docs/parsing.md");
800 guide!(ProfilePacks, "../../../site/content/docs/profile-packs.md");
801 guide!(Validation, "../../../site/content/docs/validation.md");
802
803 // Guides whose examples use the derive macros.
804 #[cfg(feature = "derive")]
805 guide!(
806 AsyncIntegration,
807 "../../../site/content/docs/async-integration.md"
808 );
809 #[cfg(feature = "derive")]
810 guide!(
811 ErrorReference,
812 "../../../site/content/docs/error-reference.md"
813 );
814 #[cfg(feature = "derive")]
815 guide!(
816 GettingStarted,
817 "../../../site/content/docs/getting-started.md"
818 );
819 #[cfg(feature = "derive")]
820 guide!(Performance, "../../../site/content/docs/performance.md");
821 #[cfg(feature = "derive")]
822 guide!(Streaming, "../../../site/content/docs/streaming.md");
823 #[cfg(feature = "derive")]
824 guide!(TypedDerive, "../../../site/content/docs/typed-derive.md");
825 #[cfg(feature = "derive")]
826 guide!(Writing, "../../../site/content/docs/writing.md");
827
828 // The diagnostics guide's examples use `miette` types.
829 #[cfg(feature = "diagnostics")]
830 guide!(Diagnostics, "../../../site/content/docs/diagnostics.md");
831
832 // The README is the crate's front page on docs.rs and crates.io, and drifts
833 // for exactly the same reason the guides did.
834 #[cfg(feature = "derive")]
835 guide!(Readme, "../../../README.md");
836}