edifact-rs-derive 0.15.0

Derive macros for EdifactSerialize / EdifactDeserialize (from edifact-rs)
Documentation

edifact-rs โšก

crates.io docs.rs CI license MSRV

EDIFACT (ISO 9735) for Rust โ€” zero-copy parsing, streaming deserialization, typed derive macros, and composable validation.

๐Ÿ“– Guides ยท ๐Ÿฆ€ API reference ยท ๐Ÿ“ฆ crates.io


Install

cargo add edifact-rs

derive is on by default. Optional features:

cargo add edifact-rs --features diagnostics   # miette-powered error rendering
cargo add edifact-rs --features serde         # Serialize/Deserialize on reports
cargo add edifact-rs --no-default-features    # core parse + write only

Quick start

Parsing borrows straight from the input โ€” segment tags and component values are &str slices into your buffer:

use edifact_rs::from_bytes;

let input = b"UNA:+.? 'UNH+1+ORDERS:D:11A:UN'BGM+220+PO-4711+9'UNT+3+1'";
let segments: Vec<_> = from_bytes(input).collect::<Result<_, _>>()?;

let bgm = &segments[1];
assert_eq!(bgm.tag, "BGM");
assert_eq!(bgm.element_str(0), Some("220"));      // document code
assert_eq!(bgm.element_str(1), Some("PO-4711"));  // document number
# Ok::<(), edifact_rs::EdifactError>(())

Map segments and whole messages onto structs, dispatching repeated segments by their qualifier:

use edifact_rs::{EdifactDeserialize, EdifactSerialize, from_bytes};

#[derive(Debug, EdifactDeserialize, EdifactSerialize)]
#[edifact(segment = "BGM")]
struct Bgm {
    #[edifact(element = 0)]
    doc_code: String,
    #[edifact(element = 1)]
    doc_number: String,
}

#[derive(Debug, EdifactDeserialize, EdifactSerialize)]
#[edifact(segment = "NAD", qualifier_from = 0)]
struct Nad {
    #[edifact(element = 0)]
    qualifier: String,
    #[edifact(element = 1)]
    party_id: Option<String>,
}

#[derive(Debug, EdifactDeserialize)]
struct OrderMessage {
    bgm: Option<Bgm>,
    #[edifact(qualifier = "BY")]
    buyer: Option<Nad>,
    #[edifact(qualifier = "SU")]
    supplier: Option<Nad>,
}

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: Vec<_> = from_bytes(input).collect::<Result<_, _>>()?;
let msg = OrderMessage::edifact_deserialize(&segments)?;

assert_eq!(msg.buyer.unwrap().party_id.as_deref(), Some("4000001000002"));
# Ok::<(), edifact_rs::EdifactError>(())

Write it back out with delimiters escaped for you:

use edifact_rs::to_edifact_string;
# use edifact_rs::EdifactSerialize;
# #[derive(EdifactSerialize)]
# #[edifact(segment = "BGM")]
# struct Bgm { #[edifact(element = 0)] doc_code: String }

let wire = to_edifact_string(&Bgm { doc_code: "220".into() })?;
assert_eq!(wire, "BGM+220'");
# Ok::<(), edifact_rs::EdifactError>(())

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 edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, Status, from_bytes};

static C082: &[ComponentRef] = &[
    ComponentRef::new(1, "3039", Status::Mandatory),
    ComponentRef::new(2, "1131", Status::Conditional),
    ComponentRef::new(3, "3055", Status::Conditional),
];
static NAD_ELEMENTS: &[ElementRef] = &[
    ElementRef::new(1, "3035", Status::Mandatory, 1),
    ElementRef::composite(2, "C082", Status::Conditional, 1, C082),
];
static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", NAD_ELEMENTS);

let segs: Vec<_> = from_bytes(b"NAD+BY+4000001000002::9'").collect::<Result<Vec<_>, _>>()?;

assert_eq!(segs[0].value_by_code(&NAD, "3039")?, Some("4000001000002"));
assert_eq!(segs[0].value_by_code(&NAD, "3055")?, Some("9"));
assert!(segs[0].value_by_code(&NAD, "2380").is_err()); // DE 2380 belongs to DTM
# Ok::<(), edifact_rs::EdifactError>(())

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 edifact_rs::{from_bytes, service};

let segments: Vec<_> = from_bytes(b"UNB+UNOC:3+SENDER+RECEIVER+260101:0900+IC4711'UNZ+0+IC4711'")
    .collect::<Result<Vec<_>, _>>()?;

// DE 0020 by name, not by counting to element 4.
assert_eq!(segments[0].value_by_code(&service::UNB, "0020")?, Some("IC4711"));
# Ok::<(), edifact_rs::EdifactError>(())

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:

just check        # fmt + clippy + tests โ€” the pre-commit gate
just test         # workspace tests, all features
just site-serve   # preview the documentation site with live reload
just ci           # everything CI runs, on this toolchain

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

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.