edifact_rs/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![deny(unsafe_code)]
3
4//! `edifact-rs` — zero-copy EDIFACT (ISO 9735) tokenizer, parser, writer, typed
5//! (de)serialization, validation engine, and extensible directory support.
6//!
7//! # Quick start
8//! ```
9//! use edifact_rs::from_bytes;
10//! let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'UNZ+0+1'";
11//! let segments: Vec<_> = from_bytes(input).collect::<Result<_, _>>().unwrap();
12//! assert_eq!(segments[0].tag, "UNB");
13//! ```
14//!
15//! # One segment type, borrowed or owned
16//!
17//! [`Segment<'a>`] holds its text as [`Cow<'a, str>`][std::borrow::Cow], which is
18//! what lets a single type cover both parsing modes:
19//!
20//! - [`from_bytes`] borrows straight out of the input — no allocation for segment
21//! data — and yields `Segment<'input>`.
22//! - [`from_reader`] has no buffer to borrow from and yields `Segment<'static>`,
23//! aliased as [`OwnedSegment`].
24//!
25//! `Segment` is covariant in `'a`, so `&[OwnedSegment]` is accepted anywhere
26//! `&[Segment<'_>]` is wanted. Every API in this crate therefore takes one shape
27//! and serves both paths — there are no `_owned` twins, and no conversion step.
28//!
29//! ```
30//! use edifact_rs::{OwnedSegment, Segment};
31//!
32//! fn count_bgm(segments: &[Segment<'_>]) -> usize {
33//! segments.iter().filter(|s| s.tag == "BGM").count()
34//! }
35//!
36//! let borrowed: Vec<Segment<'_>> =
37//! edifact_rs::from_bytes(b"BGM+220'").collect::<Result<_, _>>()?;
38//! let owned: Vec<OwnedSegment> =
39//! edifact_rs::from_reader(std::io::Cursor::new(b"BGM+220'")).collect::<Result<_, _>>()?;
40//!
41//! assert_eq!(count_bgm(&borrowed), 1);
42//! assert_eq!(count_bgm(&owned), 1);
43//! # Ok::<(), edifact_rs::EdifactError>(())
44//! ```
45//!
46//! # Crate features
47//!
48//! - `derive` (enabled by default): re-exports the derive macros from
49//! `edifact-rs-derive` — [`EdifactDeserialize`][macro@EdifactDeserialize] /
50//! [`EdifactSerialize`][macro@EdifactSerialize] for segment and message
51//! structs, and
52//! [`EdifactCompositeDeserialize`][macro@EdifactCompositeDeserialize] /
53//! [`EdifactCompositeSerialize`][macro@EdifactCompositeSerialize] for the
54//! composite-element structs they reference.
55//! - `diagnostics` (off by default): [`EdifactError`] implements
56//! `miette::Diagnostic`, for span-annotated CLI output.
57//! - `serde` (off by default): `Serialize` / `Deserialize` for
58//! [`ValidationReport`], [`ValidationIssue`], and the envelope types.
59//!
60//! Features are additive and independent: each changes only which trait impls
61//! and re-exports exist, never parsing or validation behaviour.
62//!
63//! # Parse and text contracts
64//!
65//! Parsing in `edifact-rs` is strict and deterministic:
66//!
67//! - A byte order mark and any whitespace before the first service segment are
68//! skipped — ISO 9735 authorises neither, but both arrive constantly.
69//! - A segment that ends without its terminator is a **truncation** (`E010`):
70//! accepting it would let a file cut off mid-transfer parse as complete.
71//! - Segment and element text must decode as UTF-8 (`E003`).
72//! - A release character must escape exactly one following byte; a trailing `?`
73//! at end-of-input is rejected (`E019`).
74//! - Every [`ReaderConfig`] budget **reports** a violation (`E020`, `E036`)
75//! rather than ending the iterator, which would be indistinguishable from a
76//! clean end of input.
77//! - The service characters are discovered the way ISO 9735-1 says a receiver
78//! should discover them: from a leading `UNA` if there is one, otherwise the
79//! §5.1 defaults with the repetition separator resolved from the syntax
80//! version in `UNB` S001 DE 0002 — active as `*` for version 4, inactive for
81//! versions 1–3, where `*` is ordinary data. Override both with
82//! [`ReaderConfig::with_service_string_advice`] when parsing a fragment that
83//! carries neither header.
84//! - When the repetition separator is active, repeating data elements are split
85//! into [`Element::repetitions`] rather than left glued into the value
86//! (ISO 9735-1 §8.6).
87//!
88//! Every one of these contracts applies identically to slice-based parsing
89//! ([`from_bytes`]) and reader-based parsing ([`from_reader`]); the two are held
90//! to byte-for-byte agreement by a test that runs the same inputs through both.
91//!
92//! ```
93//! use edifact_rs::from_reader;
94//! use std::io::Cursor;
95//!
96//! let input = b"UNA:;.? 'BGM;220;test?;value'";
97//! let segments: Vec<_> = from_reader(Cursor::new(&input[..]))
98//! .collect::<Result<Vec<_>, _>>()
99//! .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
107//!
108//! [`ValidationContext`] runs four layers — envelope, structure, code-list and
109//! profile — into one [`ValidationReport`]:
110//!
111//! ```
112//! use edifact_rs::{ProfileRulePack, ValidationContext, ValidationIssue, ValidationSeverity, from_bytes};
113//!
114//! let pack = ProfileRulePack::new("ORDERS-DEMO")
115//! .for_message_type("ORDERS")
116//! .with_rule_fn(|segments, issues| {
117//! if !segments.iter().any(|s| s.tag == "BGM") {
118//! issues.push(
119//! ValidationIssue::new(ValidationSeverity::Error, "ORDERS requires a BGM")
120//! .with_rule_id("DEMO-P001"),
121//! );
122//! }
123//! });
124//!
125//! let segments: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'DTM+137:1:102'UNT+3+1'")
126//! .collect::<Result<_, _>>()?;
127//! let report = ValidationContext::builder().with_profile_pack(pack).build().validate(&segments);
128//!
129//! assert_eq!(report.filter_by_rule_prefix("DEMO-").total_issues(), 1);
130//! # Ok::<(), edifact_rs::EdifactError>(())
131//! ```
132//!
133//! See the [validation](https://hupe1980.github.io/edifact-rs/docs/validation/)
134//! and [profile pack](https://hupe1980.github.io/edifact-rs/docs/profile-packs/)
135//! guides for the layers, group-scoped rules, and directory validation.
136//!
137//! # Async usage
138//!
139//! There is deliberately no native `async` API: parsing is CPU work over a
140//! buffer, not I/O, so an async parser would add a runtime dependency and a
141//! second copy of every code path to wrap work that never awaits. Read with your
142//! runtime, parse synchronously — see the
143//! [async integration guide](https://hupe1980.github.io/edifact-rs/docs/async-integration/)
144//! for the three patterns, including `spawn_blocking` for multi-gigabyte files.
145//!
146//! ```rust,no_run
147//! # async fn example(mut reader: impl tokio::io::AsyncReadExt + Unpin)
148//! # -> Result<(), Box<dyn std::error::Error>> {
149//! let mut buf = Vec::new();
150//! reader.read_to_end(&mut buf).await?;
151//! let segments: Vec<_> = edifact_rs::from_bytes(&buf).collect::<Result<Vec<_>, _>>()?;
152//! # let _ = segments;
153//! # Ok(())
154//! # }
155//! ```
156// ── core modules ──────────────────────────────────────────────────────────────
157/// EDIFACT character repertoires (`UNB` S001 DE 0001) and transcoding.
158pub mod charset;
159/// `CONTRL` — the ISO 9735-4 syntax and service report message.
160pub mod contrl;
161pub mod directory_validator;
162pub(crate) mod envelope;
163/// Error types and validation reporting primitives.
164pub(crate) mod error;
165pub mod group;
166/// Core zero-copy and owned EDIFACT data model types.
167pub(crate) mod model;
168pub(crate) mod parser;
169/// Validation report types: [`ValidationSeverity`], [`ValidationIssue`], [`ValidationReport`].
170///
171/// These types are also re-exported from the crate root.
172pub mod report;
173/// ISO 9735 service-segment definitions (`UNB`, `UNH`, `UNT`, `UNZ`, `UNG`, `UNE`, `UNS`).
174pub mod service;
175pub(crate) mod tokenizer;
176pub(crate) mod validator;
177pub(crate) mod writer;
178
179// ── typed serialization layer ─────────────────────────────────────────────────
180pub mod de;
181pub(crate) mod event;
182pub mod ser;
183
184// ── flat re-exports: core ─────────────────────────────────────────────────────
185pub use charset::{Charset, DecodingReader, decode_interchange, decode_reader, sniff_charset};
186pub use contrl::{Action, Contrl, ReportingLevel, SyntaxError};
187pub use envelope::{
188 FunctionalGroupEnvelope, GroupIdentifier, InterchangeEnvelope, LenientResult, MessageEnvelope,
189 MessageIdentifier, ValidatedInterchange, parse_ung, parse_unh, validate_envelope,
190 validate_envelope_lenient,
191};
192pub use error::{EdifactError, Insignificant, IoError};
193pub use group::{Descendants, GroupDef, SegmentGroupIndexed, group_segments_indexed};
194pub use model::{Components, Element, OwnedElement, OwnedSegment, Segment, Span};
195pub use parser::{
196 OwnedSegmentStream, Parser, ReaderConfig, from_bufread, from_bufread_with_config,
197 from_reader_with_config,
198};
199pub use report::severity_for_error;
200pub use report::{ValidationIssue, ValidationReport, ValidationSeverity};
201pub use tokenizer::{ServiceStringAdvice, Token, Tokenizer};
202pub use validator::{
203 CharsetValidator, EnvelopeValidator, ProfileRule, ProfileRulePack, SyntaxValidator,
204 ValidationContext, ValidationContextBuilder, ValidationLayer, ValidationRuleContext, Validator,
205 validate_each,
206};
207pub use writer::{AsDataElement, DataElement, MessageWriter, Writer};
208
209// ── flat re-exports: serde ────────────────────────────────────────────────────
210
211/// User-facing deserialization API.
212pub use de::{
213 CompositeElement, EdifactCompositeDeserialize, EdifactDeserialize, EdifactSegmentTag,
214 MessageWindow, MessageWindows, OwnedMessageWindow, composite_element, contiguous_groups,
215 deserialize, deserialize_each, deserialize_each_from_reader, deserialize_messages,
216 deserialize_messages_from_reader, deserialize_str, find_qualified_segment, find_segment,
217 find_segments, find_segments_typed, message_windows, message_windows_from_reader,
218 qualifier_matches_pattern,
219};
220
221// ── Proc-macro support ─────────────────────────────────────────────────────────
222
223pub use directory_validator::{
224 ComponentRef, DirectoryValidator, DirectoryValidatorBuilder, ElementPath, ElementRef,
225 LayoutAudit, LayoutFinding, LayoutSlot, OwnedComponentRef, OwnedElementRef, OwnedSegmentDef,
226 Repr, ReprKind, SegmentDefinition, SegmentLayout, Status, audit_directory,
227};
228#[cfg(feature = "derive")]
229#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
230pub use edifact_rs_derive::{
231 EdifactCompositeDeserialize, EdifactCompositeSerialize, EdifactDeserialize, EdifactSerialize,
232};
233pub use event::{EdifactEvent, EventEmitter, VecEmitter, WriterEmitter};
234pub use ser::{
235 DecimalFloat, EdifactCompositeSerialize, EdifactSerialize, emit_sparse_segment, to_bytes,
236 to_edifact_string,
237};
238
239// ── core free functions ───────────────────────────────────────────────────────
240
241use std::io::{Read, Write};
242
243/// Iterator returned by [`from_bytes`].
244pub struct FromBytesIter<'a> {
245 parser: Option<parser::Parser<'a>>,
246 pending_error: Option<EdifactError>,
247 config: ReaderConfig,
248 /// Segments successfully yielded so far.
249 segments_yielded: usize,
250 /// Complete `UNH`/`UNT` message pairs yielded so far.
251 messages_yielded: usize,
252 /// Whether a `UNH` has been yielded without its matching `UNT`.
253 in_message: bool,
254}
255
256/// Iterator returned by [`from_reader`].
257pub struct FromReaderIter<R: Read> {
258 inner: parser::OwnedSegmentStream<std::io::BufReader<R>>,
259}
260
261impl<R: Read> Iterator for FromReaderIter<R> {
262 type Item = Result<OwnedSegment, EdifactError>;
263
264 fn next(&mut self) -> Option<Self::Item> {
265 self.inner.next()
266 }
267}
268
269impl<'a> Iterator for FromBytesIter<'a> {
270 type Item = Result<Segment<'a>, EdifactError>;
271
272 fn next(&mut self) -> Option<Self::Item> {
273 if let Some(err) = self.pending_error.take() {
274 self.parser = None;
275 return Some(Err(err));
276 }
277 // Limits are checked against a segment that is actually available, so an
278 // input ending exactly at the limit finishes cleanly instead of being
279 // reported as a violation.
280 let seg = match self.parser.as_mut()?.next()? {
281 Ok(seg) => seg,
282 Err(error) => {
283 self.parser = None;
284 return Some(Err(error));
285 }
286 };
287
288 if let Some(max) = self.config.max_segments {
289 if self.segments_yielded >= max {
290 return Some(Err(self.exceeded("max_segments", max as u64)));
291 }
292 }
293 // Only a `UNH` opens a message, so only a `UNH` can push the count past
294 // the budget. Testing every segment would trip on the interchange
295 // trailer, which belongs to no message.
296 if let Some(max) = self.config.max_messages {
297 if seg.tag == "UNH" && self.messages_yielded >= max {
298 return Some(Err(self.exceeded("max_messages", max as u64)));
299 }
300 }
301 // `seg.span.end` is the byte offset just past this segment's terminator —
302 // an absolute cursor that already accounts for the UNA header, the
303 // separators, and the terminator itself.
304 if let Some(max) = self.config.max_input_bytes {
305 if seg.span.end as u64 > max {
306 return Some(Err(self.exceeded("max_input_bytes", max)));
307 }
308 }
309
310 self.segments_yielded += 1;
311 if seg.tag == "UNT" {
312 if self.in_message {
313 self.messages_yielded += 1;
314 }
315 self.in_message = false;
316 } else if seg.tag == "UNH" {
317 self.in_message = true;
318 }
319 Some(Ok(seg))
320 }
321}
322
323impl FromBytesIter<'_> {
324 /// Terminate the iterator and report the limit that tripped.
325 #[inline]
326 fn exceeded(&mut self, limit: &'static str, max: u64) -> EdifactError {
327 self.parser = None;
328 EdifactError::LimitExceeded { limit, max }
329 }
330}
331
332/// Parse `input` bytes into an iterator of [`Segment`]s.
333///
334/// Borrows directly from `input` — zero allocation for segment data.
335///
336/// # Segment-size limit
337///
338/// Applies a default 64 KiB per-segment limit, matching the reader-based path.
339/// Use [`from_bytes_with_config`] to override.
340pub fn from_bytes(input: &[u8]) -> FromBytesIter<'_> {
341 from_bytes_with_config(input, parser::ReaderConfig::default())
342}
343
344/// Parse `input` bytes into an iterator of [`Segment`]s with explicit configuration.
345///
346/// Every [`ReaderConfig`] limit is enforced as a **hard cap that yields an error**,
347/// never as a silent stop:
348///
349/// - `max_segment_bytes` — [`EdifactError::SegmentTooLong`] when a single segment
350/// exceeds the threshold.
351/// - `max_segments`, `max_messages`, `max_input_bytes` —
352/// [`EdifactError::LimitExceeded`] when the input carries more than the budget.
353///
354/// A budget that merely ended the iterator would be indistinguishable from a clean
355/// end of input, so a caller collecting into a `Vec` would silently accept a
356/// **truncated** interchange as a complete one. Input that ends exactly at a limit
357/// is not a violation and finishes normally.
358///
359/// Pass `ReaderConfig::default()` for the default 64 KiB per-segment limit with no
360/// segment-count, message-count, or byte budget.
361///
362/// # Example
363///
364/// ```
365/// use edifact_rs::{EdifactError, ReaderConfig, from_bytes_with_config};
366///
367/// // Exactly at the limit: fine.
368/// let cfg = ReaderConfig::default().max_segments(1);
369/// assert!(from_bytes_with_config(b"BGM+220'", cfg).collect::<Result<Vec<_>, _>>().is_ok());
370///
371/// // One segment too many: a loud error, not a quiet truncation.
372/// let err = from_bytes_with_config(b"BGM+220'DTM+137'", cfg)
373/// .collect::<Result<Vec<_>, _>>()
374/// .unwrap_err();
375/// assert!(matches!(err, EdifactError::LimitExceeded { limit: "max_segments", max: 1 }));
376/// ```
377pub fn from_bytes_with_config(input: &[u8], config: parser::ReaderConfig) -> FromBytesIter<'_> {
378 // A malformed `UNA` is rejected even when the caller supplied its own
379 // service characters: the input is broken either way, and silently parsing
380 // past a nine-byte header nobody validated would be the worse answer.
381 let discovered = tokenizer::ServiceStringAdvice::from_bytes(input);
382 let resolved = match (config.service_string_advice, discovered) {
383 (_, Err(error)) => Err(error),
384 (Some(override_ssa), Ok(_)) => Ok(override_ssa),
385 (None, Ok(ssa)) => Ok(ssa),
386 };
387 let (parser, pending_error) = match resolved {
388 Ok(ssa) => {
389 let t = tokenizer::Tokenizer::with_limit(input, ssa, config.max_segment_bytes);
390 (Some(parser::Parser::new(t)), None)
391 }
392 Err(error) => (None, Some(error)),
393 };
394 FromBytesIter {
395 parser,
396 pending_error,
397 config,
398 segments_yielded: 0,
399 messages_yielded: 0,
400 in_message: false,
401 }
402}
403
404/// Parse a reader into a lazy iterator of [`OwnedSegment`]s.
405///
406/// Returns a [`FromReaderIter`] that parses and yields segments on demand,
407/// keeping memory bounded. `.collect::<Result<Vec<_>, _>>()` when you do want
408/// them all in memory.
409///
410/// # Errors
411///
412/// Each `next()` call yields `Some(Ok(segment))` for a successfully parsed
413/// segment, `Some(Err(EdifactError))` for a parse or I/O failure, and `None`
414/// when the end of the stream has been reached.
415pub fn from_reader<R: Read>(reader: R) -> FromReaderIter<R> {
416 FromReaderIter {
417 inner: parser::from_reader_stream(reader),
418 }
419}
420
421/// Parse a byte slice, decoding it from the repertoire its own `UNB` declares.
422///
423/// [`decode_interchange`] followed by [`from_bytes`], in one call that a
424/// caller cannot forget to make. Forgetting is the failure mode worth designing
425/// against: a `UNOC` corpus stored as UTF-8 parses fine, so the tests pass and
426/// the first *conformant* counterparty message — the one with `ü` as the single
427/// byte `0xFC` — is rejected as invalid text.
428///
429/// Segments are owned because the decoded buffer is this function's, not the
430/// caller's: an ISO 8859-1 payload has to be transcoded to exist as UTF-8 at
431/// all. When the payload is already ASCII or `UNOY`, decoding borrows and copies
432/// nothing, but the segments are still owned — reach for
433/// [`decode_interchange`] plus [`from_bytes`] when you want to keep the
434/// zero-copy path and hold the buffer yourself.
435///
436/// # Errors
437///
438/// As [`decode_interchange`], plus any parse error.
439///
440/// # Example
441///
442/// ```
443/// // A conformant UNOC interchange: `Müller` is `4D FC 6C 6C 65 72`.
444/// let mut raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'NAD+BY+M".to_vec();
445/// raw.push(0xFC);
446/// raw.extend_from_slice(b"ller'UNZ+0+IC1'");
447///
448/// // Parsing it directly fails — it is not UTF-8, and it never claimed to be.
449/// assert!(edifact_rs::from_bytes(&raw).collect::<Result<Vec<_>, _>>().is_err());
450///
451/// let segments = edifact_rs::from_bytes_decoded(&raw)?;
452/// assert_eq!(segments[1].element_str(1), Some("Müller"));
453/// # Ok::<(), edifact_rs::EdifactError>(())
454/// ```
455pub fn from_bytes_decoded(input: &[u8]) -> Result<Vec<OwnedSegment>, EdifactError> {
456 from_bytes_decoded_with_config(input, ReaderConfig::default())
457}
458
459/// [`from_bytes_decoded`] with explicit [`ReaderConfig`] limits.
460///
461/// # Errors
462///
463/// As [`from_bytes_decoded`].
464pub fn from_bytes_decoded_with_config(
465 input: &[u8],
466 config: ReaderConfig,
467) -> Result<Vec<OwnedSegment>, EdifactError> {
468 let decoded = charset::decode_interchange(input)?;
469 from_bytes_with_config(&decoded, config)
470 .map(|r| r.map(|segment| segment.into_owned()))
471 .collect()
472}
473
474/// Parse a reader, decoding it from the repertoire the stream's own `UNB`
475/// declares — **lazily**.
476///
477/// [`decode_reader`] has to read far enough to find the `UNB` before it can
478/// answer, so it returns a `Result` — and a `?` on it turns a lazy pipeline
479/// eager, forcing the caller to box the iterator or wrap the error in a
480/// one-item chain. This does the sniff on the first `next()` instead, so the
481/// signature stays a plain `Iterator` and a decode failure arrives as its first
482/// item, exactly like a parse failure does.
483///
484/// # Example
485///
486/// ```
487/// let mut raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'NAD+BY+M".to_vec();
488/// raw.push(0xFC);
489/// raw.extend_from_slice(b"ller'UNZ+0+IC1'");
490///
491/// // No `?` before the loop: the pipeline stays lazy.
492/// let segments: Vec<_> = edifact_rs::from_reader_decoded(std::io::Cursor::new(raw))
493/// .collect::<Result<Vec<_>, _>>()?;
494/// assert_eq!(segments[1].element_str(1), Some("Müller"));
495/// # Ok::<(), edifact_rs::EdifactError>(())
496/// ```
497pub fn from_reader_decoded<R: Read>(reader: R) -> DecodingSegmentStream<R> {
498 from_reader_decoded_with_config(reader, ReaderConfig::default())
499}
500
501/// [`from_reader_decoded`] with explicit [`ReaderConfig`] limits.
502pub fn from_reader_decoded_with_config<R: Read>(
503 reader: R,
504 config: ReaderConfig,
505) -> DecodingSegmentStream<R> {
506 DecodingSegmentStream {
507 state: DecodingState::Pending(reader),
508 config,
509 }
510}
511
512/// Lazy iterator returned by [`from_reader_decoded`].
513///
514/// Sniffs the interchange's repertoire on the first `next()`, so constructing it
515/// cannot fail and the caller keeps a plain `Iterator`.
516pub struct DecodingSegmentStream<R: Read> {
517 state: DecodingState<R>,
518 config: ReaderConfig,
519}
520
521type DecodedReader<R> = charset::DecodingReader<std::io::Chain<std::io::Cursor<Vec<u8>>, R>>;
522
523enum DecodingState<R: Read> {
524 /// Nothing read yet; the repertoire is still unknown.
525 Pending(R),
526 /// Repertoire resolved; segments are streaming.
527 Running(Box<parser::OwnedSegmentStream<std::io::BufReader<DecodedReader<R>>>>),
528 /// Terminated, by exhaustion or by a decode failure already reported.
529 Done,
530}
531
532impl<R: Read> Iterator for DecodingSegmentStream<R> {
533 type Item = Result<OwnedSegment, EdifactError>;
534
535 fn next(&mut self) -> Option<Self::Item> {
536 loop {
537 match &mut self.state {
538 DecodingState::Done => return None,
539 DecodingState::Running(stream) => return stream.next(),
540 DecodingState::Pending(_) => {
541 let DecodingState::Pending(reader) =
542 std::mem::replace(&mut self.state, DecodingState::Done)
543 else {
544 unreachable!("guarded by the match arm")
545 };
546 // The sniff happens here rather than at construction, which
547 // is what keeps the signature a plain `Iterator`.
548 match charset::decode_reader(reader) {
549 Ok(decoded) => {
550 self.state = DecodingState::Running(Box::new(
551 parser::from_reader_with_config(decoded, self.config),
552 ));
553 }
554 Err(error) => return Some(Err(error)),
555 }
556 }
557 }
558 }
559 }
560}
561
562/// Serialize `segments` to an [`std::io::Write`] implementation.
563///
564/// # Errors
565///
566/// Returns an error if writing fails or if segment serialization fails.
567pub fn to_writer<'a, 'b, W, I>(w: W, segments: I) -> Result<(), EdifactError>
568where
569 'b: 'a,
570 W: Write,
571 I: IntoIterator<Item = &'a Segment<'b>>,
572{
573 let mut wr = writer::Writer::new(w);
574 for seg in segments {
575 wr.write_segment(seg)?;
576 }
577 wr.finish().map(|_| ())
578}
579
580/// Serialize `segments` to an owned `Vec<u8>`.
581///
582/// Accepts segments from either parsing path: `&[OwnedSegment]` coerces to
583/// `&[Segment<'_>]`.
584///
585/// # Errors
586///
587/// Returns an error if serialization fails.
588///
589/// # Example
590///
591/// ```
592/// let segments: Vec<_> = edifact_rs::from_bytes(b"BGM+220+PO-1'")
593/// .collect::<Result<Vec<_>, _>>()?;
594/// assert_eq!(edifact_rs::segments_to_bytes(&segments)?, b"BGM+220+PO-1'".to_vec());
595/// # Ok::<(), edifact_rs::EdifactError>(())
596/// ```
597pub fn segments_to_bytes<'a, 'b, I>(segments: I) -> Result<Vec<u8>, EdifactError>
598where
599 'b: 'a,
600 I: IntoIterator<Item = &'a Segment<'b>>,
601{
602 let mut buf = Vec::new();
603 to_writer(&mut buf, segments)?;
604 Ok(buf)
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn from_bytes_rejects_invalid_una() {
613 let err = from_bytes(b"UNA::.? 'BGM:220'")
614 .collect::<Result<Vec<_>, _>>()
615 .expect_err("invalid UNA should fail slice parsing");
616 assert!(matches!(err, EdifactError::InvalidUna));
617 }
618}
619
620/// Compiles and runs every ```` ```rust ```` block in the published guides as a
621/// doctest.
622///
623/// A rename that breaks a guide breaks the build.
624///
625/// Blocks that genuinely cannot run (they need a live socket, a real directory
626/// file, or a downstream crate) should be marked ```` ```rust,ignore ```` or
627/// ```` ```rust,no_run ```` in the guide itself.
628#[cfg(doctest)]
629mod doc_guides {
630 macro_rules! guide {
631 ($name:ident, $path:literal) => {
632 #[doc = include_str!($path)]
633 pub struct $name;
634 };
635 }
636
637 guide!(
638 CharacterSets,
639 "../../../site/content/docs/character-sets.md"
640 );
641 guide!(Contrl, "../../../site/content/docs/contrl.md");
642 guide!(CoreConcepts, "../../../site/content/docs/core-concepts.md");
643 guide!(Parsing, "../../../site/content/docs/parsing.md");
644 guide!(ProfilePacks, "../../../site/content/docs/profile-packs.md");
645 guide!(Validation, "../../../site/content/docs/validation.md");
646
647 // Guides whose examples use the derive macros.
648 #[cfg(feature = "derive")]
649 guide!(
650 AsyncIntegration,
651 "../../../site/content/docs/async-integration.md"
652 );
653 #[cfg(feature = "derive")]
654 guide!(
655 ErrorReference,
656 "../../../site/content/docs/error-reference.md"
657 );
658 #[cfg(feature = "derive")]
659 guide!(
660 GettingStarted,
661 "../../../site/content/docs/getting-started.md"
662 );
663 #[cfg(feature = "derive")]
664 guide!(Performance, "../../../site/content/docs/performance.md");
665 #[cfg(feature = "derive")]
666 guide!(Streaming, "../../../site/content/docs/streaming.md");
667 #[cfg(feature = "derive")]
668 guide!(TypedDerive, "../../../site/content/docs/typed-derive.md");
669 #[cfg(feature = "derive")]
670 guide!(Writing, "../../../site/content/docs/writing.md");
671
672 // The diagnostics guide's examples use `miette` types.
673 #[cfg(feature = "diagnostics")]
674 guide!(Diagnostics, "../../../site/content/docs/diagnostics.md");
675
676 // The README is the crate's front page on docs.rs and crates.io, and drifts
677 // for exactly the same reason the guides did.
678 #[cfg(feature = "derive")]
679 guide!(Readme, "../../../README.md");
680}