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