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//! - When a `UNA` declares a repetition separator at position 7, repeating data
88//! elements are split into [`Element::repetitions`] rather than left glued
89//! into the value (ISO 9735-4 §3.1).
90//!
91//! These contracts apply to both slice-based parsing (`from_bytes`) and
92//! reader-based parsing (`from_reader`).
93//!
94//! ```
95//! use edifact_rs::from_reader_collect;
96//! use std::io::Cursor;
97//!
98//! let input = b"UNA:;.? 'BGM;220;test?;value'";
99//! let segments = from_reader_collect(Cursor::new(&input[..])).unwrap();
100//! assert_eq!(segments.len(), 1);
101//! assert_eq!(segments[0].tag, "BGM");
102//! assert_eq!(segments[0].element_str(0), Some("220"));
103//! assert_eq!(segments[0].element_str(1), Some("test;value"));
104//! ```
105//!
106//! # Validation Quick Start
107//!
108//! The `Validator` trait and `ValidationContext` provide a flexible framework
109//! for building custom validators. Users can generate validators from official
110//! UNECE sources or implement their own.
111//!
112//! See the [`Validator`] trait documentation and the `cookbook_fixture_validation.rs`
113//! example for details on creating custom validators.
114//!
115//! # Custom Profile Packs
116//!
117//! `ProfileRulePack` is the extension point for downstream MIG/profile crates.
118//! Packs can be authored with public APIs only and plugged into a
119//! [`ValidationContext`]:
120//!
121//! ```
122//! use edifact_rs::{
123//! from_bytes, ProfileRulePack, ValidationContext, ValidationIssue, ValidationSeverity,
124//! };
125//!
126//! let segments: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO123+9'UNT+3+1'")
127//! .collect::<Result<_, _>>()?;
128//!
129//! let pack = ProfileRulePack::new("ORDERS-DEMO")
130//! .for_message_type("ORDERS")
131//! .with_stateless_rule_fn(|segments, issues| {
132//! if let Some(bgm) = segments.iter().find(|segment| segment.tag == "BGM") {
133//! if let Some(code) = bgm.get_element(0).and_then(|e| e.get_component(0)) {
134//! if code == "220" {
135//! issues.push(
136//! ValidationIssue::new(
137//! ValidationSeverity::Warning,
138//! "demo pack rejects BGM 220 for illustration",
139//! )
140//! .with_rule_id("DEMO-P001")
141//! .with_segment("BGM")
142//! .with_element_index(0),
143//! );
144//! }
145//! }
146//! }
147//! });
148//!
149//! let report = ValidationContext::builder()
150//! .with_profile_pack(pack)
151//! .build()
152//! .validate_lenient(&segments);
153//!
154//! assert!(report.has_warnings());
155//! let partner_report = report.filter_by_rule_prefix("DEMO-");
156//! assert!(partner_report.total_issues() >= 1);
157//! # Ok::<(), edifact_rs::EdifactError>(())
158//! ```
159//!
160//! # Async Usage
161//!
162//! `edifact-rs` does not provide a native `async` API. All parsing is
163//! synchronous and driven by the standard `std::io::Read` / `std::io::BufRead`
164//! traits. The recommended integration pattern with async runtimes is:
165//!
166//! 1. Use your async runtime's read utilities to read the entire message into a
167//! `Vec<u8>` (e.g. `tokio::io::AsyncReadExt::read_to_end`).
168//! 2. Parse the in-memory slice with [`from_bytes`].
169//!
170//! ```rust,no_run
171//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
172//! // With tokio:
173//! // let mut buf = Vec::new();
174//! // reader.read_to_end(&mut buf).await?;
175//! // let segments: Vec<_> = edifact_rs::from_bytes(&buf).collect::<Result<_, _>>()?;
176//! # Ok(())
177//! # }
178//! ```
179// ── core modules ──────────────────────────────────────────────────────────────
180/// EDIFACT character repertoires (`UNB` S001 DE 0001) and transcoding.
181pub mod charset;
182pub mod directory_validator;
183pub(crate) mod envelope;
184/// Error types and validation reporting primitives.
185pub(crate) mod error;
186pub mod group;
187/// Core zero-copy and owned EDIFACT data model types.
188pub(crate) mod model;
189pub(crate) mod parser;
190/// Validation report types: [`ValidationSeverity`], [`ValidationIssue`], [`ValidationReport`].
191///
192/// These types are also re-exported from the crate root.
193pub mod report;
194/// ISO 9735 service-segment definitions (`UNB`, `UNH`, `UNT`, `UNZ`, `UNG`, `UNE`, `UNS`).
195pub mod service;
196pub(crate) mod tokenizer;
197pub(crate) mod validator;
198pub(crate) mod writer;
199
200// ── typed serialization layer ─────────────────────────────────────────────────
201pub mod de;
202pub(crate) mod event;
203pub mod ser;
204
205// ── flat re-exports: core ─────────────────────────────────────────────────────
206pub use charset::{Charset, DecodingReader, decode_interchange, decode_reader, sniff_charset};
207pub use envelope::{
208 FunctionalGroupEnvelope, GroupIdentifier, InterchangeEnvelope, LenientResult, MessageEnvelope,
209 MessageIdentifier, ValidatedInterchange, parse_ung, parse_unh, validate_envelope,
210 validate_envelope_from_owned, validate_envelope_lenient, validate_envelope_lenient_from_owned,
211};
212pub use error::{EdifactError, IoError};
213pub use group::{
214 GroupDef, SegmentGroupIndexed, group_owned_segments_indexed, group_segments_indexed,
215};
216pub use model::{
217 BorrowedElement, BorrowedSegment, Components, Element, OwnedComponents, OwnedElement,
218 OwnedSegment, Segment, Span,
219};
220pub use parser::{
221 OwnedSegmentStream, Parser, ReaderConfig, from_bufread, from_bufread_stream,
222 from_bufread_stream_with_config, from_reader_with_config,
223};
224pub use report::{ValidationIssue, ValidationReport, ValidationSeverity};
225pub use tokenizer::{ServiceStringAdvice, Token, Tokenizer};
226pub use validator::{
227 CharsetValidator, EnvelopeValidator, ProfileRule, ProfileRulePack, ValidationContext,
228 ValidationContextBuilder, ValidationLayer, ValidationRuleContext, Validator, validate_each,
229};
230pub use writer::{AsDataElement, DataElement, MessageWriter, Writer};
231
232// ── flat re-exports: serde ────────────────────────────────────────────────────
233
234/// User-facing deserialization API.
235pub use de::{
236 CompositeElement, DispatchedMessage, EdifactCompositeDeserialize, EdifactDeserialize,
237 EdifactSegmentTag, MessageDispatch, MessageWindow, MessageWindowsIter, MessageWindowsSliceIter,
238 OwnedMessageWindow, SegmentAccessor, composite_element, contiguous_groups_by_qualifier,
239 contiguous_groups_iter, deserialize, deserialize_all_from_reader, deserialize_all_streaming,
240 deserialize_first_from_reader, deserialize_first_streaming, deserialize_messages_bytes,
241 deserialize_messages_from_reader, deserialize_str, element_str, find_qualified_segment,
242 find_qualified_segment_owned, find_segment, find_segment_owned, find_segment_typed,
243 find_segments_iter, find_segments_typed, get_components_iter,
244 groups_are_contiguous_by_qualifier, message_windows_from_reader, optional_component,
245 optional_element, qualifier_matches_pattern, required_component, required_element,
246};
247
248/// Splits a byte slice into [`MessageWindow`] views, one per `UNH`/`UNT` envelope,
249/// enabling parallel or lazy per-message processing without copying data.
250///
251/// # Example
252/// ```rust
253/// use edifact_rs::from_bytes_windows;
254///
255/// let input = b"UNB+UNOA:1+S+R+200101:0900+1'\
256/// UNH+1+ORDERS:D:96A:UN'BGM+220+A+9'UNT+3+1'\
257/// UNH+2+ORDERS:D:96A:UN'BGM+220+B+9'UNT+3+2'\
258/// UNZ+2+1'";
259/// let windows: Vec<_> = from_bytes_windows(input).collect::<Result<Vec<_>, _>>()?;
260///
261/// assert_eq!(windows.len(), 2);
262/// // Each window spans exactly one UNH..UNT pair.
263/// assert_eq!(windows[0].segments.first().unwrap().tag, "UNH");
264/// assert_eq!(windows[0].segments.last().unwrap().tag, "UNT");
265/// # Ok::<(), edifact_rs::EdifactError>(())
266/// ```
267pub use de::message_windows_bytes as from_bytes_windows;
268
269// ── Proc-macro support ─────────────────────────────────────────────────────────
270
271pub use directory_validator::{
272 ComponentRef, DirectoryValidator, DirectoryValidatorBuilder, ElementPath, ElementRef,
273 OwnedComponentRef, OwnedElementRef, OwnedSegmentDef, SegmentDefinition, SegmentLayout, Status,
274};
275#[cfg(feature = "derive")]
276#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
277pub use edifact_rs_derive::{
278 EdifactCompositeDeserialize, EdifactCompositeSerialize, EdifactDeserialize, EdifactSerialize,
279};
280pub use event::{EdifactEvent, EventEmitter, OwnedEdifactEvent, VecEmitter, WriterEmitter};
281pub use ser::{
282 DecimalFloat, DecimalFloatDisplay, EdifactCompositeSerialize, EdifactSerialize,
283 emit_sparse_segment, to_bytes, to_edifact_string,
284};
285
286// ── core free functions ───────────────────────────────────────────────────────
287
288use std::io::{Read, Write};
289
290/// Iterator returned by [`from_bytes`].
291pub struct FromBytesIter<'a> {
292 parser: Option<parser::Parser<'a>>,
293 pending_error: Option<EdifactError>,
294 config: ReaderConfig,
295 /// Segments successfully yielded so far.
296 segments_yielded: usize,
297 /// Complete `UNH`/`UNT` message pairs yielded so far.
298 messages_yielded: usize,
299 /// Whether a `UNH` has been yielded without its matching `UNT`.
300 in_message: bool,
301}
302
303/// Iterator returned by [`from_reader`].
304pub struct FromReaderIter<R: Read> {
305 inner: parser::OwnedSegmentStream<std::io::BufReader<R>>,
306}
307
308impl<R: Read> Iterator for FromReaderIter<R> {
309 type Item = Result<OwnedSegment, EdifactError>;
310
311 fn next(&mut self) -> Option<Self::Item> {
312 self.inner.next()
313 }
314}
315
316impl<'a> Iterator for FromBytesIter<'a> {
317 type Item = Result<Segment<'a>, EdifactError>;
318
319 fn next(&mut self) -> Option<Self::Item> {
320 if let Some(err) = self.pending_error.take() {
321 self.parser = None;
322 return Some(Err(err));
323 }
324 // Limits are checked against a segment that is actually available, so an
325 // input ending exactly at the limit finishes cleanly instead of being
326 // reported as a violation.
327 let seg = match self.parser.as_mut()?.next()? {
328 Ok(seg) => seg,
329 Err(error) => {
330 self.parser = None;
331 return Some(Err(error));
332 }
333 };
334
335 if let Some(max) = self.config.max_segments {
336 if self.segments_yielded >= max {
337 return Some(Err(self.exceeded("max_segments", max as u64)));
338 }
339 }
340 // Only a `UNH` opens a message, so only a `UNH` can push the count past
341 // the budget. Testing every segment would trip on the interchange
342 // trailer, which belongs to no message.
343 if let Some(max) = self.config.max_messages {
344 if seg.tag == "UNH" && self.messages_yielded >= max {
345 return Some(Err(self.exceeded("max_messages", max as u64)));
346 }
347 }
348 // `seg.span.end` is the byte offset just past this segment's terminator —
349 // an absolute cursor that already accounts for the UNA header, the
350 // separators, and the terminator itself.
351 if let Some(max) = self.config.max_input_bytes {
352 if seg.span.end as u64 > max {
353 return Some(Err(self.exceeded("max_input_bytes", max)));
354 }
355 }
356
357 self.segments_yielded += 1;
358 if seg.tag == "UNT" {
359 if self.in_message {
360 self.messages_yielded += 1;
361 }
362 self.in_message = false;
363 } else if seg.tag == "UNH" {
364 self.in_message = true;
365 }
366 Some(Ok(seg))
367 }
368}
369
370impl FromBytesIter<'_> {
371 /// Terminate the iterator and report the limit that tripped.
372 #[inline]
373 fn exceeded(&mut self, limit: &'static str, max: u64) -> EdifactError {
374 self.parser = None;
375 EdifactError::LimitExceeded { limit, max }
376 }
377}
378
379/// Parse `input` bytes into an iterator of [`Segment`]s.
380///
381/// Borrows directly from `input` — zero allocation for segment data.
382///
383/// # Segment-size limit
384///
385/// Applies a default 64 KiB per-segment limit, matching the reader-based path.
386/// Use [`from_bytes_with_config`] to override.
387pub fn from_bytes(input: &[u8]) -> FromBytesIter<'_> {
388 from_bytes_with_config(input, parser::ReaderConfig::default())
389}
390
391/// Parse `input` bytes into an iterator of [`Segment`]s with explicit configuration.
392///
393/// Every [`ReaderConfig`] limit is enforced as a **hard cap that yields an error**,
394/// never as a silent stop:
395///
396/// - `max_segment_bytes` — [`EdifactError::SegmentTooLong`] when a single segment
397/// exceeds the threshold.
398/// - `max_segments`, `max_messages`, `max_input_bytes` —
399/// [`EdifactError::LimitExceeded`] when the input carries more than the budget.
400///
401/// A budget that merely ended the iterator would be indistinguishable from a clean
402/// end of input, so a caller collecting into a `Vec` would silently accept a
403/// **truncated** interchange as a complete one. Input that ends exactly at a limit
404/// is not a violation and finishes normally.
405///
406/// Pass `ReaderConfig::default()` for the default 64 KiB per-segment limit with no
407/// segment-count, message-count, or byte budget.
408///
409/// # Example
410///
411/// ```
412/// use edifact_rs::{EdifactError, ReaderConfig, from_bytes_with_config};
413///
414/// // Exactly at the limit: fine.
415/// let cfg = ReaderConfig::default().max_segments(1);
416/// assert!(from_bytes_with_config(b"BGM+220'", cfg).collect::<Result<Vec<_>, _>>().is_ok());
417///
418/// // One segment too many: a loud error, not a quiet truncation.
419/// let err = from_bytes_with_config(b"BGM+220'DTM+137'", cfg)
420/// .collect::<Result<Vec<_>, _>>()
421/// .unwrap_err();
422/// assert!(matches!(err, EdifactError::LimitExceeded { limit: "max_segments", max: 1 }));
423/// ```
424pub fn from_bytes_with_config(input: &[u8], config: parser::ReaderConfig) -> FromBytesIter<'_> {
425 let (parser, pending_error) = match tokenizer::ServiceStringAdvice::from_bytes(input) {
426 Ok(ssa) => {
427 let t = tokenizer::Tokenizer::with_limit(input, ssa, config.max_segment_bytes);
428 (Some(parser::Parser::new(t)), None)
429 }
430 Err(error) => (None, Some(error)),
431 };
432 FromBytesIter {
433 parser,
434 pending_error,
435 config,
436 segments_yielded: 0,
437 messages_yielded: 0,
438 in_message: false,
439 }
440}
441
442/// Parse a reader into a lazy iterator of [`OwnedSegment`]s.
443///
444/// Returns a [`FromReaderIter`] that parses and yields segments on demand,
445/// keeping memory bounded. Use [`from_reader_collect`] to eagerly materialise
446/// all segments into a `Vec`.
447///
448/// # Errors
449///
450/// Each `next()` call yields `Some(Ok(segment))` for a successfully parsed
451/// segment, `Some(Err(EdifactError))` for a parse or I/O failure, and `None`
452/// when the end of the stream has been reached.
453pub fn from_reader<R: Read>(reader: R) -> FromReaderIter<R> {
454 FromReaderIter {
455 inner: parser::from_reader_stream(reader),
456 }
457}
458
459/// Parse a reader into an owned `Vec` of all segments.
460///
461/// Eagerly collects the full interchange into memory. If you only need a
462/// subset of segments, prefer [`from_reader`] (lazy iterator) to avoid
463/// unnecessary allocations.
464///
465/// # Errors
466///
467/// Returns an error if the input contains malformed EDIFACT syntax,
468/// invalid UTF-8 segment text, dangling release sequences, or underlying I/O failures.
469pub fn from_reader_collect<R: Read>(reader: R) -> Result<Vec<OwnedSegment>, EdifactError> {
470 parser::from_reader(reader)
471}
472
473/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s.
474///
475/// Unlike [`from_bytes`] (which yields borrowed [`Segment`]s tied to the input
476/// lifetime), every segment returned here is fully owned. This is convenient
477/// when you need to store or return segments without retaining a reference to
478/// the original byte slice.
479///
480/// # Example
481///
482/// ```
483/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned(b"BGM+220+1+9'")
484/// .collect::<Result<_, _>>()
485/// .unwrap();
486/// assert_eq!(segs[0].tag, "BGM");
487/// ```
488pub fn from_bytes_owned(
489 input: &[u8],
490) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
491 from_bytes(input).map(|r| r.map(OwnedSegment::from))
492}
493
494/// Parse `input` bytes eagerly into an iterator of [`OwnedSegment`]s with a
495/// custom [`ReaderConfig`].
496///
497/// Identical to [`from_bytes_owned`] but applies the limits and settings from
498/// `config` (e.g. `max_segment_bytes`, `max_segments`, `max_input_bytes`).
499///
500/// # Example
501///
502/// ```
503/// use edifact_rs::ReaderConfig;
504/// let config = ReaderConfig::default().max_segments(10);
505/// let segs: Vec<edifact_rs::OwnedSegment> = edifact_rs::from_bytes_owned_with_config(
506/// b"BGM+220+1+9'",
507/// config,
508/// )
509/// .collect::<Result<_, _>>()
510/// .unwrap();
511/// assert_eq!(segs[0].tag, "BGM");
512/// ```
513pub fn from_bytes_owned_with_config(
514 input: &[u8],
515 config: ReaderConfig,
516) -> impl Iterator<Item = Result<OwnedSegment, EdifactError>> + '_ {
517 from_bytes_with_config(input, config).map(|r| r.map(OwnedSegment::from))
518}
519
520/// Serialize `segments` to an [`std::io::Write`] implementation.
521///
522/// # Errors
523///
524/// Returns an error if writing fails or if segment serialization fails.
525pub fn to_writer<'a, 'b, W, I>(w: W, segments: I) -> Result<(), EdifactError>
526where
527 'b: 'a,
528 W: Write,
529 I: IntoIterator<Item = &'a Segment<'b>>,
530{
531 let mut wr = writer::Writer::new(w);
532 for seg in segments {
533 wr.write_segment(seg)?;
534 }
535 wr.finish().map(|_| ())
536}
537
538/// Serialize `segments` to an owned `Vec<u8>`.
539///
540/// # Errors
541///
542/// Returns an error if serialization fails.
543pub fn segments_to_bytes<'a, 'b, I>(segments: I) -> Result<Vec<u8>, EdifactError>
544where
545 'b: 'a,
546 I: IntoIterator<Item = &'a Segment<'b>>,
547{
548 let mut buf = Vec::new();
549 to_writer(&mut buf, segments)?;
550 Ok(buf)
551}
552
553/// Serialize a slice of [`OwnedSegment`]s to an owned `Vec<u8>`.
554///
555/// Convenience wrapper around [`to_writer`] that accepts owned segments
556/// directly. Each segment is converted to its borrowed form on the fly
557/// and written immediately — no intermediate `Vec<Segment<'_>>` is
558/// allocated, so peak memory stays proportional to one segment at a time
559/// rather than the full slice.
560///
561/// # Errors
562///
563/// Returns an error if serialization fails.
564pub fn segments_to_bytes_owned(segments: &[OwnedSegment]) -> Result<Vec<u8>, EdifactError> {
565 let mut buf = Vec::new();
566 let mut wr = writer::Writer::new(&mut buf);
567 for seg in segments {
568 wr.write_segment(&seg.as_borrowed())?;
569 }
570 wr.finish()?;
571 Ok(buf)
572}
573
574/// Validate the envelope structure of an owned-segment slice.
575///
576/// Convenience wrapper that accepts `&[OwnedSegment]` without requiring a
577/// manual conversion to borrowed segments. Unlike the previous implementation,
578/// no intermediate `Vec<Segment<'_>>` is allocated — segments are read directly.
579///
580/// # Errors
581///
582/// Returns an error if the envelope is structurally invalid.
583pub fn validate_envelope_owned(
584 segments: &[OwnedSegment],
585) -> Result<ValidatedInterchange, EdifactError> {
586 envelope::validate_envelope_from_owned(segments)
587}
588
589/// Lenient envelope validation over owned segments — collects all errors.
590///
591/// Convenience wrapper around [`validate_envelope_lenient_from_owned`].
592/// Returns a [`LenientResult`] with `Some(result)` and empty errors on success.
593/// On count-only violations, returns `Some(partial)` with errors.
594/// On structural failures, returns `None` with errors.
595pub fn validate_envelope_lenient_owned(segments: &[OwnedSegment]) -> LenientResult {
596 envelope::validate_envelope_lenient_from_owned(segments)
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn from_bytes_rejects_invalid_una() {
605 let err = from_bytes(b"UNA::.? 'BGM:220'")
606 .collect::<Result<Vec<_>, _>>()
607 .expect_err("invalid UNA should fail slice parsing");
608 assert!(matches!(err, EdifactError::InvalidUna));
609 }
610}
611
612/// Compiles and runs every ```` ```rust ```` block in the published guides as a
613/// doctest.
614///
615/// The guides drifted from the API — snippets referenced private module paths
616/// and methods that did not exist — because nothing ever compiled them. Wiring
617/// them in here means a rename that breaks a guide breaks the build.
618///
619/// Blocks that genuinely cannot run (they need a live socket, a real directory
620/// file, or a downstream crate) should be marked ```` ```rust,ignore ```` or
621/// ```` ```rust,no_run ```` in the guide itself.
622#[cfg(doctest)]
623mod doc_guides {
624 macro_rules! guide {
625 ($name:ident, $path:literal) => {
626 #[doc = include_str!($path)]
627 pub struct $name;
628 };
629 }
630
631 guide!(
632 CharacterSets,
633 "../../../site/content/docs/character-sets.md"
634 );
635 guide!(CoreConcepts, "../../../site/content/docs/core-concepts.md");
636 guide!(Parsing, "../../../site/content/docs/parsing.md");
637 guide!(ProfilePacks, "../../../site/content/docs/profile-packs.md");
638 guide!(Validation, "../../../site/content/docs/validation.md");
639
640 // Guides whose examples use the derive macros.
641 #[cfg(feature = "derive")]
642 guide!(
643 AsyncIntegration,
644 "../../../site/content/docs/async-integration.md"
645 );
646 #[cfg(feature = "derive")]
647 guide!(
648 ErrorReference,
649 "../../../site/content/docs/error-reference.md"
650 );
651 #[cfg(feature = "derive")]
652 guide!(
653 GettingStarted,
654 "../../../site/content/docs/getting-started.md"
655 );
656 #[cfg(feature = "derive")]
657 guide!(Performance, "../../../site/content/docs/performance.md");
658 #[cfg(feature = "derive")]
659 guide!(Streaming, "../../../site/content/docs/streaming.md");
660 #[cfg(feature = "derive")]
661 guide!(TypedDerive, "../../../site/content/docs/typed-derive.md");
662 #[cfg(feature = "derive")]
663 guide!(Writing, "../../../site/content/docs/writing.md");
664
665 // The diagnostics guide's examples use `miette` types.
666 #[cfg(feature = "diagnostics")]
667 guide!(Diagnostics, "../../../site/content/docs/diagnostics.md");
668
669 // The README is the crate's front page on docs.rs and crates.io, and drifts
670 // for exactly the same reason the guides did.
671 #[cfg(feature = "derive")]
672 guide!(Readme, "../../../README.md");
673}