Skip to main content

edifact_rs/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3//! `edifact-rs` — zero-copy EDIFACT tokenizer, parser, writer, serde traits,
4//! validation engine, and extensible directory support.
5//!
6//! `edifact-rs` is the main entry point of this workspace. The core parsing,
7//! writing, and validation infrastructure is always available. Custom directory
8//! validators can be implemented by downstream crates or generated through
9//! external build tooling.
10//!
11//! # Quick start
12//! ```
13//! use edifact_rs::from_bytes;
14//! let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
15//! let segments: Vec<_> = from_bytes(input).collect::<Result<_, _>>().unwrap();
16//! assert_eq!(segments[0].tag, "UNB");
17//! ```
18//!
19//! # Crate features
20//!
21//! - `derive` (enabled by default): re-exports the derive macros from
22//!   `edifact-rs-derive`.
23//! - `diagnostics` (disabled by default): enables rich diagnostic output via `miette`.
24//!   When enabled, errors implement `miette::Diagnostic` for enhanced error reporting.
25//!   This feature adds an optional dependency and has no impact on parsing performance.
26//!
27//! The crate is expected to compile both with defaults and with
28//! `--no-default-features` for consumers who only want the core parsing and
29//! writing functionality.
30//!
31//! ## Feature matrix workflows
32//!
33//! - default features:
34//!   `cargo test -p edifact-rs`
35//! - no default features:
36//!   `cargo test -p edifact-rs --no-default-features`
37//! - all features:
38//!   `cargo test -p edifact-rs --all-features`
39//!
40//! # Diagnostic Feature
41//!
42//! When the `diagnostics` feature is enabled, [`EdifactError`] gains additional
43//! traits and methods that enable rich, human-readable error output:
44//!
45//! ```text
46//! Error: invalid delimiter byte 0xAB at offset 42
47//!
48//!  ╭─ input.edi:2:3
49//!  │
50//!  2 │ UNB+UNOA:1+....[invalid]...
51//!  │         ^^^ invalid byte here
52//!  │
53//! Error Code: E002
54//! Help: The byte 0xAB is not a valid delimiter. Check UNA configuration
55//! ```
56//!
57//! This feature is useful for CLI tools and error reporting, but is not required
58//! for applications that handle errors programmatically.
59//!
60//! # Parse And Text Contracts
61//!
62//! Parsing in `edifact-rs` is strict and deterministic:
63//!
64//! - Segment and element text must decode as UTF-8 (`E003` on failure).
65//! - Release characters must escape exactly one following byte.
66//!   A trailing `?` at end-of-input is rejected (`E018`).
67//! - Malformed delimiters and truncated segments are reported with stable
68//!   error codes rather than panicking.
69//!
70//! These contracts apply to both slice-based parsing (`from_bytes`) and
71//! reader-based parsing (`from_reader`).
72//!
73//! ```
74//! use edifact_rs::from_reader;
75//! use std::io::Cursor;
76//!
77//! let input = b"UNA:;.? 'BGM;220;test?;value'";
78//! let segments = from_reader(Cursor::new(&input[..])).unwrap();
79//! assert_eq!(segments.len(), 1);
80//! assert_eq!(segments[0].tag, "BGM");
81//! assert_eq!(segments[0].elements[0].components[0], "220");
82//! assert_eq!(segments[0].elements[1].components[0], "test;value");
83//! ```
84//!
85//! # Validation Quick Start
86//!
87//! The `Validator` trait and `ValidationContext` provide a flexible framework
88//! for building custom validators. Users can generate validators from official
89//! UNECE sources or implement their own.
90//!
91//! See the [`Validator`] trait documentation and the `cookbook_fixture_validation.rs`
92//! example for details on creating custom validators.
93//!
94//! # Custom Profile Packs
95//!
96//! `ProfileRulePack` is the extension point for downstream MIG/profile crates.
97//! Packs can be authored with public APIs only and plugged into a
98//! [`ValidationContext`]:
99//!
100//! ```
101//! use edifact_rs::{
102//!     from_bytes, ProfileRulePack, ValidationContext, ValidationIssue, ValidationSeverity,
103//! };
104//!
105//! let segments: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'")
106//!     .collect::<Result<_, _>>()?;
107//!
108//! let pack = ProfileRulePack::builder("ORDERS-DEMO")
109//!     .for_message_type("ORDERS")
110//!     .with_rule_fn(|segments| {
111//!         let bgm = segments.iter().find(|segment| segment.tag == "BGM")?;
112//!         let document_code = bgm.get_element(0)?.get_component(0)?;
113//!         (document_code == "220").then(|| {
114//!             ValidationIssue::new(
115//!                 ValidationSeverity::Warning,
116//!                 "demo pack rejects BGM 220 for illustration",
117//!             )
118//!             .with_rule_id("DEMO-P001")
119//!             .with_segment("BGM")
120//!             .with_element_index(0)
121//!         })
122//!     });
123//!
124//! let report = ValidationContext::builder()
125//!     .with_profile_pack(pack)
126//!     .build()
127//!     .validate_lenient(&segments);
128//!
129//! assert!(report.has_warnings());
130//! let partner_report = report.filter_by_rule_prefix("DEMO-");
131//! assert!(partner_report.total_issues() >= 1);
132//! # Ok::<(), edifact_rs::EdifactError>(())
133//! ```
134//!
135//! # Async Usage
136//!
137//! `edifact-rs` does not provide a native `async` API.  All parsing is
138//! synchronous and driven by the standard `std::io::Read` / `std::io::BufRead`
139//! traits.  The recommended integration pattern with async runtimes is:
140//!
141//! 1. Use your async runtime's read utilities to read the entire message into a
142//!    `Vec<u8>` (e.g. `tokio::io::AsyncReadExt::read_to_end`).
143//! 2. Parse the in-memory slice with [`from_bytes`].
144//!
145//! ```rust,no_run
146//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
147//! // With tokio:
148//! // let mut buf = Vec::new();
149//! // reader.read_to_end(&mut buf).await?;
150//! // let segments: Vec<_> = edifact_rs::from_bytes(&buf).collect::<Result<_, _>>()?;
151//! # Ok(())
152//! # }
153//! ```
154//!
155//! A native zero-copy streaming async API is tracked as a future roadmap item.
156// ── core modules ──────────────────────────────────────────────────────────────
157pub mod directory_validator;
158pub mod envelope;
159/// Error types and validation reporting primitives.
160pub mod error;
161/// Core zero-copy and owned EDIFACT data model types.
162pub mod model;
163pub mod parser;
164pub mod tokenizer;
165pub mod validator;
166pub mod writer;
167
168// ── typed serialization layer ─────────────────────────────────────────────────
169pub mod de;
170pub mod event;
171pub mod ser;
172
173// ── flat re-exports: core ─────────────────────────────────────────────────────
174pub use envelope::validate_envelope;
175pub use error::{EdifactError, IoError, ValidationIssue, ValidationReport, ValidationSeverity};
176pub use model::{BorrowedElement, BorrowedSegment, Element, OwnedElement, OwnedSegment, Segment, Span};
177pub use parser::{
178    Parser, ReaderConfig, from_bufread, from_bufread_stream, from_bufread_stream_with_config,
179    from_reader_with_config,
180};
181pub use tokenizer::{ServiceStringAdvice, Tokenizer};
182pub use validator::{
183    ProfileRule, ProfileRulePack, ValidationContext, ValidationContextBuilder, ValidationLayer,
184    Validator, validate_each,
185};
186pub use writer::Writer;
187
188// ── flat re-exports: serde ────────────────────────────────────────────────────
189pub use de::{
190    CompositeElement, EdifactCompositeDeserialize, EdifactDeserialize, EdifactSegmentTag,
191    MessageWindowsIter, MessageWindowsSliceIter, SegmentAccessor, composite_element,
192    contiguous_groups_by_qualifier, deserialize, deserialize_all_from_reader,
193    deserialize_all_streaming, deserialize_first_from_reader, deserialize_first_streaming,
194    deserialize_messages_bytes, deserialize_messages_from_reader, deserialize_str, element_str,
195    find_qualified_segment, find_qualified_segment_owned, find_segment, find_segment_owned,
196    find_segment_typed, find_segments_iter, find_segments_typed, get_components_iter,
197    groups_are_contiguous_by_qualifier,
198    message_windows_bytes, message_windows_from_reader, optional_component, optional_element,
199    qualifier_matches_pattern, required_component, required_element,
200};
201#[cfg(feature = "derive")]
202#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
203pub use edifact_rs_derive::{EdifactDeserialize, EdifactSerialize};
204pub use event::{EdifactEvent, EventEmitter, OwnedEdifactEvent, VecEmitter, WriterEmitter};
205pub use ser::{EdifactCompositeSerialize, EdifactSerialize, to_string};
206pub use directory_validator::{DirectoryValidator, ElementRef, SegmentDefinition, Status};
207
208// ── core free functions ───────────────────────────────────────────────────────
209
210use std::io::{Read, Write};
211
212/// Iterator returned by [`from_bytes`].
213pub struct FromBytesIter<'a> {
214    parser: Option<parser::Parser<'a>>,
215    pending_error: Option<EdifactError>,
216}
217
218/// Iterator returned by [`from_reader_iter`].
219pub struct FromReaderIter<R: Read> {
220    inner: parser::OwnedSegmentStream<std::io::BufReader<R>>,
221}
222
223impl<R: Read> Iterator for FromReaderIter<R> {
224    type Item = Result<OwnedSegment, EdifactError>;
225
226    fn next(&mut self) -> Option<Self::Item> {
227        self.inner.next()
228    }
229}
230
231impl<'a> Iterator for FromBytesIter<'a> {
232    type Item = Result<Segment<'a>, EdifactError>;
233
234    fn next(&mut self) -> Option<Self::Item> {
235        if let Some(err) = self.pending_error.take() {
236            return Some(Err(err));
237        }
238        self.parser.as_mut()?.next()
239    }
240}
241
242/// Parse `input` bytes into an iterator of [`Segment`]s.
243///
244/// Borrows directly from `input` — zero allocation for segment data.
245pub fn from_bytes(input: &[u8]) -> FromBytesIter<'_> {
246    match tokenizer::ServiceStringAdvice::from_bytes_strict(input) {
247        Ok(ssa) => {
248            let t = tokenizer::Tokenizer::new(input, ssa);
249            FromBytesIter {
250                parser: Some(parser::Parser::new(t)),
251                pending_error: None,
252            }
253        }
254        Err(error) => FromBytesIter {
255            parser: None,
256            pending_error: Some(error),
257        },
258    }
259}
260
261/// Parse a reader into owned segments.
262///
263/// # Errors
264///
265/// Returns an error if the input contains malformed EDIFACT syntax,
266/// invalid UTF-8 segment text, dangling release sequences, or underlying I/O failures.
267pub fn from_reader<R: Read>(reader: R) -> Result<Vec<OwnedSegment>, EdifactError> {
268    parser::from_reader(reader)
269}
270
271/// Parse a reader into owned segments as a streaming iterator.
272///
273/// This keeps memory bounded by yielding segments incrementally instead of
274/// materializing the full interchange up front.
275pub fn from_reader_iter<R: Read>(reader: R) -> FromReaderIter<R> {
276    FromReaderIter {
277        inner: parser::from_reader_stream(reader),
278    }
279}
280
281/// Serialize `segments` to an [`std::io::Write`] implementation.
282///
283/// # Errors
284///
285/// Returns an error if writing fails or if segment serialization fails.
286pub fn to_writer<'a, 'b, W, I>(w: W, segments: I) -> Result<(), EdifactError>
287where
288    'b: 'a,
289    W: Write,
290    I: IntoIterator<Item = &'a Segment<'b>>,
291{
292    let mut wr = writer::Writer::new(w);
293    for seg in segments {
294        wr.write_segment(seg)?;
295    }
296    wr.finish().map(|_| ())
297}
298
299/// Serialize `segments` to an owned `Vec<u8>`.
300///
301/// # Errors
302///
303/// Returns an error if serialization fails.
304pub fn to_bytes<'a, 'b, I>(segments: I) -> Result<Vec<u8>, EdifactError>
305where
306    'b: 'a,
307    I: IntoIterator<Item = &'a Segment<'b>>,
308{
309    let mut buf = Vec::new();
310    to_writer(&mut buf, segments)?;
311    Ok(buf)
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn from_bytes_rejects_invalid_una() {
320        let err = from_bytes(b"UNA::.? 'BGM:220'")
321            .collect::<Result<Vec<_>, _>>()
322            .expect_err("invalid UNA should fail slice parsing");
323        assert!(matches!(err, EdifactError::InvalidUna));
324    }
325}