Skip to main content

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`.
24//! - `diagnostics` (disabled by default): enables rich diagnostic output via `miette`.
25//!   When enabled, errors implement `miette::Diagnostic` for enhanced error reporting.
26//!   This feature adds an optional dependency and has no impact on parsing performance.
27//!
28//! The crate is expected to compile both with defaults and with
29//! `--no-default-features` for consumers who only want the core parsing and
30//! writing functionality.
31//!
32//! ## Feature matrix workflows
33//!
34//! - default features:
35//!   `cargo test -p edifact-rs`
36//! - no default features:
37//!   `cargo test -p edifact-rs --no-default-features`
38//! - all features:
39//!   `cargo test -p edifact-rs --all-features`
40//!
41//! # Diagnostic Feature
42//!
43//! When the `diagnostics` feature is enabled, [`EdifactError`] gains additional
44//! traits and methods that enable rich, human-readable error output:
45//!
46//! ```text
47//! Error: invalid delimiter byte 0xAB at offset 42
48//!
49//!  ╭─ input.edi:2:3
50//!  │
51//!  2 │ UNB+UNOA:1+....[invalid]...
52//!  │         ^^^ invalid byte here
53//!  │
54//! Error Code: E002
55//! Help: The byte 0xAB is not a valid delimiter. Check UNA configuration
56//! ```
57//!
58//! This feature is useful for CLI tools and error reporting, but is not required
59//! for applications that handle errors programmatically.
60//!
61//! # Parse And Text Contracts
62//!
63//! Parsing in `edifact-rs` is strict and deterministic:
64//!
65//! - Segment and element text must decode as UTF-8 (`E003` on failure).
66//! - Release characters must escape exactly one following byte.
67//!   A trailing `?` at end-of-input is rejected (`E019`).
68//! - Malformed delimiters and truncated segments are reported with stable
69//!   error codes rather than panicking.
70//!
71//! These contracts apply to both slice-based parsing (`from_bytes`) and
72//! reader-based parsing (`from_reader`).
73//!
74//! ```
75//! use edifact_rs::from_reader_collect;
76//! use std::io::Cursor;
77//!
78//! let input = b"UNA:;.? 'BGM;220;test?;value'";
79//! let segments = from_reader_collect(Cursor::new(&input[..])).unwrap();
80//! assert_eq!(segments.len(), 1);
81//! assert_eq!(segments[0].tag, "BGM");
82//! assert_eq!(segments[0].element_str(0), Some("220"));
83//! assert_eq!(segments[0].element_str(1), Some("test;value"));
84//! ```
85//!
86//! # Validation Quick Start
87//!
88//! The `Validator` trait and `ValidationContext` provide a flexible framework
89//! for building custom validators. Users can generate validators from official
90//! UNECE sources or implement their own.
91//!
92//! See the [`Validator`] trait documentation and the `cookbook_fixture_validation.rs`
93//! example for details on creating custom validators.
94//!
95//! # Custom Profile Packs
96//!
97//! `ProfileRulePack` is the extension point for downstream MIG/profile crates.
98//! Packs can be authored with public APIs only and plugged into a
99//! [`ValidationContext`]:
100//!
101//! ```
102//! use edifact_rs::{
103//!     from_bytes, ProfileRulePack, ValidationContext, ValidationIssue, ValidationSeverity,
104//! };
105//!
106//! let segments: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'")
107//!     .collect::<Result<_, _>>()?;
108//!
109//! let pack = ProfileRulePack::new("ORDERS-DEMO")
110//!     .for_message_type("ORDERS")
111//!     .with_stateless_rule_fn(|segments, issues| {
112//!         if let Some(bgm) = segments.iter().find(|segment| segment.tag == "BGM") {
113//!             if let Some(code) = bgm.get_element(0).and_then(|e| e.get_component(0)) {
114//!                 if code == "220" {
115//!                     issues.push(
116//!                         ValidationIssue::new(
117//!                             ValidationSeverity::Warning,
118//!                             "demo pack rejects BGM 220 for illustration",
119//!                         )
120//!                         .with_rule_id("DEMO-P001")
121//!                         .with_segment("BGM")
122//!                         .with_element_index(0),
123//!                     );
124//!                 }
125//!             }
126//!         }
127//!     });
128//!
129//! let report = ValidationContext::builder()
130//!     .with_profile_pack(pack)
131//!     .build()
132//!     .validate_lenient(&segments);
133//!
134//! assert!(report.has_warnings());
135//! let partner_report = report.filter_by_rule_prefix("DEMO-");
136//! assert!(partner_report.total_issues() >= 1);
137//! # Ok::<(), edifact_rs::EdifactError>(())
138//! ```
139//!
140//! # Async Usage
141//!
142//! `edifact-rs` does not provide a native `async` API.  All parsing is
143//! synchronous and driven by the standard `std::io::Read` / `std::io::BufRead`
144//! traits.  The recommended integration pattern with async runtimes is:
145//!
146//! 1. Use your async runtime's read utilities to read the entire message into a
147//!    `Vec<u8>` (e.g. `tokio::io::AsyncReadExt::read_to_end`).
148//! 2. Parse the in-memory slice with [`from_bytes`].
149//!
150//! ```rust,no_run
151//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
152//! // With tokio:
153//! // let mut buf = Vec::new();
154//! // reader.read_to_end(&mut buf).await?;
155//! // let segments: Vec<_> = edifact_rs::from_bytes(&buf).collect::<Result<_, _>>()?;
156//! # Ok(())
157//! # }
158//! ```
159//!
160//! A native zero-copy streaming async API is tracked as a future roadmap item.
161// ── core modules ──────────────────────────────────────────────────────────────
162pub mod directory_validator;
163pub(crate) mod envelope;
164/// Error types and validation reporting primitives.
165pub(crate) mod error;
166pub mod group;
167/// Core zero-copy and owned EDIFACT data model types.
168pub(crate) mod model;
169pub(crate) mod parser;
170/// Validation report types: [`ValidationSeverity`], [`ValidationIssue`], [`ValidationReport`].
171///
172/// These types are also re-exported from the crate root.
173pub mod report;
174pub(crate) mod tokenizer;
175pub(crate) mod validator;
176pub(crate) mod writer;
177
178// ── typed serialization layer ─────────────────────────────────────────────────
179pub mod de;
180pub(crate) mod event;
181pub mod ser;
182
183// ── flat re-exports: core ─────────────────────────────────────────────────────
184pub use envelope::{
185    FunctionalGroupEnvelope, GroupIdentifier, InterchangeEnvelope, LenientResult, MessageEnvelope,
186    MessageIdentifier, ValidatedInterchange, parse_ung, parse_unh, validate_envelope,
187    validate_envelope_from_owned, validate_envelope_lenient, validate_envelope_lenient_from_owned,
188};
189pub use error::{EdifactError, IoError};
190pub use group::{
191    GroupDef, SegmentGroupIndexed, group_owned_segments_indexed, group_segments_indexed,
192};
193pub use model::{
194    BorrowedElement, BorrowedSegment, Element, OwnedElement, OwnedSegment, Segment, Span,
195};
196pub use parser::{
197    Parser, ReaderConfig, from_bufread, from_bufread_stream, from_bufread_stream_with_config,
198    from_reader_with_config,
199};
200pub use report::{ValidationIssue, ValidationReport, ValidationSeverity};
201pub use tokenizer::{ServiceStringAdvice, Tokenizer};
202pub use validator::{
203    EnvelopeValidator, ProfileRule, ProfileRulePack, ValidationContext, ValidationContextBuilder,
204    ValidationLayer, ValidationRuleContext, Validator, validate_each,
205};
206pub use writer::{MessageWriter, Writer};
207
208// ── flat re-exports: serde ────────────────────────────────────────────────────
209
210/// User-facing deserialization API.
211pub use de::{
212    CompositeElement, DispatchedMessage, EdifactCompositeDeserialize, EdifactDeserialize,
213    EdifactSegmentTag, MessageDispatch, MessageWindow, MessageWindowsIter, MessageWindowsSliceIter,
214    OwnedMessageWindow, SegmentAccessor, composite_element, contiguous_groups_by_qualifier,
215    deserialize, deserialize_all_from_reader, deserialize_all_streaming,
216    deserialize_first_from_reader, deserialize_first_streaming, deserialize_messages_bytes,
217    deserialize_messages_from_reader, deserialize_str, element_str, find_qualified_segment,
218    find_qualified_segment_owned, find_segment, find_segment_owned, find_segment_typed,
219    find_segments_iter, find_segments_typed, get_components_iter,
220    groups_are_contiguous_by_qualifier, message_windows_from_reader, optional_component,
221    optional_element, qualifier_matches_pattern, required_component, required_element,
222};
223
224/// Splits a byte slice into [`MessageWindow`] views, one per `UNH`/`UNT` envelope,
225/// enabling parallel or lazy per-message processing without copying data.
226///
227/// # Example
228/// ```rust,ignore
229/// use edifact_rs::from_bytes_windows;
230/// let windows: Vec<_> = from_bytes_windows(input).collect();
231/// ```
232pub use de::message_windows_bytes as from_bytes_windows;
233
234// ── Proc-macro support ─────────────────────────────────────────────────────────
235
236pub use directory_validator::{
237    DirectoryValidator, DirectoryValidatorBuilder, ElementRef, OwnedElementRef, OwnedSegmentDef,
238    SegmentDefinition, Status,
239};
240#[cfg(feature = "derive")]
241#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
242pub use edifact_rs_derive::{EdifactDeserialize, EdifactSerialize};
243pub use event::{EdifactEvent, EventEmitter, OwnedEdifactEvent, VecEmitter, WriterEmitter};
244pub use ser::{
245    DecimalFloat, DecimalFloatDisplay, EdifactCompositeSerialize, EdifactSerialize, to_bytes,
246    to_edifact_string,
247};
248
249// ── core free functions ───────────────────────────────────────────────────────
250
251use std::io::{Read, Write};
252
253/// Iterator returned by [`from_bytes`].
254pub struct FromBytesIter<'a> {
255    parser: Option<parser::Parser<'a>>,
256    pending_error: Option<EdifactError>,
257    /// Remaining segment allowance (`None` = unlimited).
258    segments_remaining: Option<usize>,
259    /// Maximum byte budget (`None` = unlimited).
260    bytes_remaining: Option<u64>,
261    /// Byte offset of the start of the current parse position (approximated
262    /// as the sum of previously yielded segment spans — the borrowed tokenizer
263    /// does not expose a byte counter, so we track it from `Segment::span`).
264    bytes_consumed: u64,
265}
266
267/// Iterator returned by [`from_reader`].
268pub struct FromReaderIter<R: Read> {
269    inner: parser::OwnedSegmentStream<std::io::BufReader<R>>,
270}
271
272impl<R: Read> Iterator for FromReaderIter<R> {
273    type Item = Result<OwnedSegment, EdifactError>;
274
275    fn next(&mut self) -> Option<Self::Item> {
276        self.inner.next()
277    }
278}
279
280impl<'a> Iterator for FromBytesIter<'a> {
281    type Item = Result<Segment<'a>, EdifactError>;
282
283    fn next(&mut self) -> Option<Self::Item> {
284        if let Some(err) = self.pending_error.take() {
285            return Some(Err(err));
286        }
287        // max_segments guard
288        if let Some(ref mut remaining) = self.segments_remaining {
289            if *remaining == 0 {
290                self.parser = None;
291                return None;
292            }
293        }
294        // max_input_bytes guard — uses absolute byte offset from the input start.
295        // `bytes_consumed` holds `seg.span.end` of the last yielded segment, which
296        // is an absolute position in the input slice and therefore naturally includes
297        // the 9-byte UNA header and the segment terminator character.
298        if let Some(max) = self.bytes_remaining {
299            if self.bytes_consumed >= max {
300                self.parser = None;
301                return None;
302            }
303        }
304        let item = self.parser.as_mut()?.next();
305        if let Some(Ok(ref seg)) = item {
306            // Decrement segment allowance
307            if let Some(ref mut remaining) = self.segments_remaining {
308                *remaining = remaining.saturating_sub(1);
309            }
310            // Track the absolute input position at the end of this segment.
311            // `seg.span.end` is the byte offset just past the segment terminator —
312            // a monotonically increasing absolute cursor that automatically accounts
313            // for the UNA header, element/component separators, and terminators.
314            self.bytes_consumed = seg.span.end as u64;
315            if let Some(max) = self.bytes_remaining {
316                if self.bytes_consumed >= max {
317                    self.parser = None;
318                }
319            }
320        }
321        item
322    }
323}
324
325/// Parse `input` bytes into an iterator of [`Segment`]s.
326///
327/// Borrows directly from `input` — zero allocation for segment data.
328///
329/// # Segment-size limit
330///
331/// Applies a default 64 KiB per-segment limit, matching the reader-based path.
332/// Use [`from_bytes_with_config`] to override.
333pub fn from_bytes(input: &[u8]) -> FromBytesIter<'_> {
334    from_bytes_with_config(input, parser::ReaderConfig::default())
335}
336
337/// Parse `input` bytes into an iterator of [`Segment`]s with explicit configuration.
338///
339/// All three [`ReaderConfig`] limits are enforced:
340/// - `max_segment_bytes`: returns [`EdifactError::SegmentTooLong`] if a single segment
341///   exceeds the threshold.
342/// - `max_segments`: stops the iterator after this many segments have been yielded.
343/// - `max_input_bytes`: **stop-after** limit — the iterator stops once the cumulative
344///   byte position (tracked via `Segment::span.end`) reaches or exceeds this value.
345///   The last segment whose `span.end` exceeds the limit is **still returned**;
346///   no further segments are fetched after that.  This means at most one segment
347///   worth of bytes can be processed beyond the limit, which is sufficient for a
348///   DoS guard but is not a strict hard cap.  If your use case requires that every
349///   yielded segment fits entirely within `max_input_bytes` bytes, collect and
350///   filter the output, or set the limit conservatively below the true boundary.
351///
352/// Pass `ReaderConfig::default()` to use the default 64 KiB per-segment limit with
353/// no segment-count or byte-budget cap.
354///
355/// # Example
356///
357/// ```
358/// use edifact_rs::{ReaderConfig, from_bytes_with_config};
359///
360/// let cfg = ReaderConfig::default().max_segment_bytes(128);
361/// let result: Result<Vec<_>, _> = from_bytes_with_config(b"BGM+220+1+9'", cfg).collect();
362/// assert!(result.is_ok());
363/// ```
364pub fn from_bytes_with_config(input: &[u8], config: parser::ReaderConfig) -> FromBytesIter<'_> {
365    let segments_remaining = config.max_segments;
366    let bytes_remaining = config.max_input_bytes;
367    match tokenizer::ServiceStringAdvice::from_bytes(input) {
368        Ok(ssa) => {
369            let t = tokenizer::Tokenizer::with_limit(input, ssa, config.max_segment_bytes);
370            FromBytesIter {
371                parser: Some(parser::Parser::new(t)),
372                pending_error: None,
373                segments_remaining,
374                bytes_remaining,
375                bytes_consumed: 0,
376            }
377        }
378        Err(error) => FromBytesIter {
379            parser: None,
380            pending_error: Some(error),
381            segments_remaining,
382            bytes_remaining,
383            bytes_consumed: 0,
384        },
385    }
386}
387
388/// Parse a reader into a lazy iterator of [`OwnedSegment`]s.
389///
390/// Returns a [`FromReaderIter`] that parses and yields segments on demand,
391/// keeping memory bounded. Use [`from_reader_collect`] to eagerly materialise
392/// all segments into a `Vec`.
393///
394/// # Errors
395///
396/// Each `next()` call yields `Some(Ok(segment))` for a successfully parsed
397/// segment, `Some(Err(EdifactError))` for a parse or I/O failure, and `None`
398/// when the end of the stream has been reached.
399pub fn from_reader<R: Read>(reader: R) -> FromReaderIter<R> {
400    FromReaderIter {
401        inner: parser::from_reader_stream(reader),
402    }
403}
404
405/// Parse a reader into an owned `Vec` of all segments.
406///
407/// Eagerly collects the full interchange into memory. If you only need a
408/// subset of segments, prefer [`from_reader`] (lazy iterator) to avoid
409/// unnecessary allocations.
410///
411/// # Errors
412///
413/// Returns an error if the input contains malformed EDIFACT syntax,
414/// invalid UTF-8 segment text, dangling release sequences, or underlying I/O failures.
415pub fn from_reader_collect<R: Read>(reader: R) -> Result<Vec<OwnedSegment>, EdifactError> {
416    parser::from_reader(reader)
417}
418
419/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s.
420///
421/// Unlike [`from_bytes`] (which yields borrowed [`Segment`]s tied to the input
422/// lifetime), every segment returned here is fully owned.  This is convenient
423/// when you need to store or return segments without retaining a reference to
424/// the original byte slice.
425///
426/// # Example
427///
428/// ```
429/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned(b"BGM+220+1+9'")
430///     .collect::<Result<_, _>>()
431///     .unwrap();
432/// assert_eq!(segs[0].tag, "BGM");
433/// ```
434pub fn from_bytes_owned(
435    input: &[u8],
436) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
437    from_bytes(input).map(|r| r.map(OwnedSegment::from))
438}
439
440/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s with a
441/// custom [`ReaderConfig`].
442///
443/// Identical to [`from_bytes_owned`] but applies the limits and settings from
444/// `config` (e.g. `max_segment_bytes`, `max_segments`, `max_input_bytes`).
445///
446/// # Example
447///
448/// ```
449/// use edifact_rs::ReaderConfig;
450/// let config = ReaderConfig::default().max_segments(10);
451/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned_with_config(
452///     b"BGM+220+1+9'",
453///     config,
454/// )
455/// .collect::<Result<_, _>>()
456/// .unwrap();
457/// assert_eq!(segs[0].tag, "BGM");
458/// ```
459pub fn from_bytes_owned_with_config(
460    input: &[u8],
461    config: ReaderConfig,
462) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
463    from_bytes_with_config(input, config).map(|r| r.map(OwnedSegment::from))
464}
465
466/// Parse a reader into owned segments as a streaming iterator.
467///
468/// This keeps memory bounded by yielding segments incrementally instead of
469/// materializing the full interchange up front.
470///
471/// # Deprecation
472///
473/// Use [`from_reader`] instead — this function is an alias kept for internal use.
474pub(crate) fn from_reader_iter<R: Read>(reader: R) -> FromReaderIter<R> {
475    FromReaderIter {
476        inner: parser::from_reader_stream(reader),
477    }
478}
479
480/// Serialize `segments` to an [`std::io::Write`] implementation.
481///
482/// # Errors
483///
484/// Returns an error if writing fails or if segment serialization fails.
485pub fn to_writer<'a, 'b, W, I>(w: W, segments: I) -> Result<(), EdifactError>
486where
487    'b: 'a,
488    W: Write,
489    I: IntoIterator<Item = &'a Segment<'b>>,
490{
491    let mut wr = writer::Writer::new(w);
492    for seg in segments {
493        wr.write_segment(seg)?;
494    }
495    wr.finish().map(|_| ())
496}
497
498/// Serialize `segments` to an owned `Vec<u8>`.
499///
500/// # Errors
501///
502/// Returns an error if serialization fails.
503pub fn segments_to_bytes<'a, 'b, I>(segments: I) -> Result<Vec<u8>, EdifactError>
504where
505    'b: 'a,
506    I: IntoIterator<Item = &'a Segment<'b>>,
507{
508    let mut buf = Vec::new();
509    to_writer(&mut buf, segments)?;
510    Ok(buf)
511}
512
513/// Serialize a slice of [`OwnedSegment`]s to an owned `Vec<u8>`.
514///
515/// Convenience wrapper around [`to_writer`] that accepts owned segments
516/// directly.  Each segment is converted to its borrowed form on the fly
517/// and written immediately — no intermediate `Vec<Segment<'_>>` is
518/// allocated, so peak memory stays proportional to one segment at a time
519/// rather than the full slice.
520///
521/// # Errors
522///
523/// Returns an error if serialization fails.
524pub fn segments_to_bytes_owned(segments: &[OwnedSegment]) -> Result<Vec<u8>, EdifactError> {
525    let mut buf = Vec::new();
526    let mut wr = writer::Writer::new(&mut buf);
527    for seg in segments {
528        wr.write_segment(&seg.as_borrowed())?;
529    }
530    wr.finish()?;
531    Ok(buf)
532}
533
534/// Validate the envelope structure of an owned-segment slice.
535///
536/// Convenience wrapper that accepts `&[OwnedSegment]` without requiring a
537/// manual conversion to borrowed segments.  Unlike the previous implementation,
538/// no intermediate `Vec<Segment<'_>>` is allocated — segments are read directly.
539///
540/// # Errors
541///
542/// Returns an error if the envelope is structurally invalid.
543pub fn validate_envelope_owned(
544    segments: &[OwnedSegment],
545) -> Result<ValidatedInterchange, EdifactError> {
546    envelope::validate_envelope_from_owned(segments)
547}
548
549/// Lenient envelope validation over owned segments — collects all errors.
550///
551/// Convenience wrapper around [`validate_envelope_lenient_from_owned`].
552/// Returns a [`LenientResult`] with `Some(result)` and empty errors on success.
553/// On count-only violations, returns `Some(partial)` with errors.
554/// On structural failures, returns `None` with errors.
555pub fn validate_envelope_lenient_owned(segments: &[OwnedSegment]) -> LenientResult {
556    envelope::validate_envelope_lenient_from_owned(segments)
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    #[test]
564    fn from_bytes_rejects_invalid_una() {
565        let err = from_bytes(b"UNA::.? 'BGM:220'")
566            .collect::<Result<Vec<_>, _>>()
567            .expect_err("invalid UNA should fail slice parsing");
568        assert!(matches!(err, EdifactError::InvalidUna));
569    }
570}