edifact-rs โก
EDIFACT (ISO 9735) for Rust โ zero-copy parsing, streaming deserialization, typed derive macros, and composable validation.
๐ Guides ยท ๐ฆ API reference ยท ๐ฆ crates.io
Install
derive is on by default. Optional features:
Quick start
Parsing borrows straight from the input โ segment tags and component values are
&str slices into your buffer:
use from_bytes;
let input = b"UNA:+.? 'UNH+1+ORDERS:D:11A:UN'BGM+220+PO-4711+9'UNT+3+1'";
let segments: = from_bytes.?;
let bgm = &segments;
assert_eq!;
assert_eq!; // document code
assert_eq!; // document number
# Ok::
Map segments and whole messages onto structs, dispatching repeated segments by their qualifier:
use ;
let input = b"UNH+1+ORDERS:D:11A:UN'BGM+220+PO-4711+9'\
NAD+BY+4000001000002::9'NAD+SU+4000001000001::9'UNT+5+1'";
let segments: = from_bytes.?;
let msg = edifact_deserialize?;
assert_eq!;
# Ok::
Write it back out with delimiters escaped for you:
use to_edifact_string;
# use EdifactSerialize;
#
#
#
let wire = to_edifact_string?;
assert_eq!;
# Ok::
What makes it different
EDIFACT is deceptively simple โ flat text, a handful of delimiters โ which is
why hand-rolled parsers are common and quietly wrong. The delimiters are
redefinable per interchange, any of them may appear inside a value when
release-escaped, and syntax version 4 adds a repetition separator that changes
an element's shape rather than its text. edifact-rs takes a position on each
of the places that usually goes wrong:
| Zero-copy by default | Tags and values borrow from the input slice. The only per-segment allocation is the element vector; an owned string appears solely where a release escape had to be resolved. |
| Constant-memory streaming | Reader iterators yield one segment at a time; message windows group them into UNHโฆUNT units. A multi-gigabyte interchange costs one message of peak memory. |
| Identifiers, not indices | Address a field by its UN/EDIFACT data element identifier. The derive resolves it during const evaluation, so a stale identifier fails the build instead of reading the element next door. |
| Repetitions are parsed | A declared repetition separator splits an element into real occurrences (ISO 9735-4 ยง3.1) instead of leaving 1*ON in the value as literal text. |
| Limits report, never truncate | Segment, message, and byte budgets raise an error. A budget that quietly ended iteration is indistinguishable from clean end-of-input, so a caller would accept a truncated interchange as a whole one. |
| Layered validation | Envelope, structure, code-list, and profile checks write into one report carrying stable error codes, byte spans, and filterable rule identifiers. |
| Character sets are decoded, not assumed | UTF-8 is not a superset of UNOC, so a conformant German interchange is unparseable as UTF-8. decode_interchange reads the repertoire from UNB S001 and transcodes โ borrowing, not copying, when the payload is already ASCII. |
No unsafe |
#![deny(unsafe_code)], with property and fuzz tests over parse, write, and validate on every commit. |
Addressing a field by identifier
Transpose one positional index and you read the wrong data element โ and it still validates clean. Given a segment definition, address the value by its identifier instead, and a wrong reference becomes a lookup error:
use ;
static C082: & = &;
static NAD_ELEMENTS: & = &;
static NAD: SegmentDefinition = new;
let segs: = from_bytes.?;
assert_eq!;
assert_eq!;
assert!; // DE 2380 belongs to DTM
# Ok::
The derive performs the same resolution at compile time โ see Typed Derive.
Documentation
Full guides live at hupe1980.github.io/edifact-rs. Every Rust snippet on the site is compiled and run as part of the test suite, so none of it can drift from the crate.
| Guide | |
|---|---|
| Getting Started | Install, first parse, feature flags |
| Core Concepts | Wire format, UNA, release characters, repetitions, Rust type mapping |
| Character Sets | UNOAโUNOK/UNOY decoding, encoding, and repertoire validation |
| Parsing | Entry points, byte spans, and the ReaderConfig budgets |
| Writing | Writer, escaping, custom UNA, repeating elements |
| Typed Derive | Every derive attribute, including identifier-addressed fields |
| Streaming | Reader iterators, message windows, typed extraction |
| Validation | Validator, ValidationContext, the four layers |
| Profile Packs | Authoring, composing, and filtering business rules |
| Diagnostics | miette integration |
| Async Integration | Bridging to tokio |
| Error Reference | Every stable code E001โE041 |
| Performance | Allocation budgets, benchmarks, tuning |
Runnable cookbooks live in
crates/edifact-rs/examples/ โ try one with
cargo run --example cookbook_parse_map_validate_write.
Scope
edifact-rs is the engine, not a directory distribution.
It ships the parser, writer, validation pipeline, the table types for segment
definitions, and the ISO 9735 service segments โ UNB, UNG, UNH, UNT,
UNE, UNZ, UNS โ as ready-to-use layouts in edifact_rs::service. Those are
fixed by the syntax standard rather than by a directory release, so there is one
correct answer and no version to pick:
use ;
let segments: = from_bytes
.?;
// DE 0020 by name, not by counting to element 4.
assert_eq!;
# Ok::
It does not ship UN/EDIFACT directory data โ BGM, DTM, NAD, C507
and the rest โ which is versioned per release, large, and licensed separately.
Supply those as static tables at compile time, or load them at startup with
DirectoryValidatorBuilder.
Development
The justfile mirrors CI, so a green just ci means a green build:
MSRV and edition
Rust 1.85, edition 2024. The MSRV is enforced by CI on every push and a raise is treated as a breaking change.
License
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work shall be dual-licensed as above, without any additional terms or conditions.