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` — [`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}