Validation
The Validator trait, ValidationContext, and the layered envelope / structure / code-list / profile validation pipeline.
On this page
- Key types
- The Validator trait
- ValidationContext and layers
- validate_lenient vs validate_strict
- ValidationReport — working with results
- ValidationIssue — building findings
- Custom validator implementations
- Progressive streaming validation
- Group-aware validation
- Directory validator
- Data element representations
- Auditing a hand-written layout
- Code-addressed element access
- Next steps
edifact-rs provides a layered, composable validation pipeline that separates
structural, code-list, and profile-level checks — each pluggable independently.
Key types
| Type | Role |
|---|---|
Validator | Trait — implement to create a custom validator |
ValidationContext | Orchestrates multiple validators across layers |
ValidationLayer | Enum — Structure, CodeList, Profile |
ValidationReport | Aggregated result — errors, warnings, infos |
ValidationIssue | A single finding — severity, message, rule ID, offsets |
ValidationSeverity | Critical, Error, Warning, Info |
ProfileRulePack | Composable bundle of closure-based profile rules |
validate_each | Helper — run a per-segment function over a slice |
The Validator trait
Implement Validator to encapsulate validation logic:
use edifact_rs::{Validator, ValidationReport, ValidationRuleContext, Segment, validate_each, EdifactError};
struct BgmCodeValidator;
impl Validator for BgmCodeValidator {
fn validate_batch(
&self,
segments: &[Segment<'_>],
report: &mut ValidationReport,
_context: &ValidationRuleContext<'_>,
) {
validate_each(segments, report, |seg| {
if seg.tag == "BGM" {
let code = seg.element_str(0).unwrap_or("");
if !matches!(code, "220" | "231" | "261") {
return Err(EdifactError::InvalidCodeValue {
tag: "BGM".to_owned(),
element_index: 0,
value: code.to_owned(),
code_list: "1001".to_owned(),
span: seg.span,
suggestion: Some("Use 220 (original order), 231 (quote) or 261 (confirmation)"),
});
}
}
Ok(())
});
}
}
The validate_batch method receives the complete segment slice for the current
validation scope (one UNH..UNT window or the whole interchange, depending on how
the context is driven).
validate_each is a helper that iterates segments and maps each Err result to a
ValidationIssue appended to report.
The trait also provides validate_group_batch (default: no-op) which is called once
per segment-group occurrence when using the group-aware validation path. Override it
to access the isolated segment list for each group instance.
ValidationContext and layers
Validators are registered per layer. Layers run in order:
Structure— check mandatory segments, ordering, countsCodeList— check element values against UNTDID code listsProfile— check business/MIG rules (see Profile Packs)
use edifact_rs::{
ValidationContext, ValidationLayer, Validator, ValidationReport,
ValidationRuleContext, Segment, from_bytes,
};
# struct BgmCodeValidator;
# impl Validator for BgmCodeValidator {
# fn validate_batch(&self, _: &[Segment<'_>], _: &mut ValidationReport, _: &ValidationRuleContext<'_>) {}
# fn set_message_type(&mut self, _: Option<&str>) {}
# }
let segs: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:11A:UN'BGM+220+PO-4711+9'UNT+3+1'")
.collect::<Result<_, _>>()?;
let ctx = ValidationContext::builder()
.with_message_type("ORDERS") // passed to set_message_type on each validator
.with_validator(ValidationLayer::CodeList, BgmCodeValidator)
.build();
let report = ctx.validate_lenient(&segs);
# Ok::<(), edifact_rs::EdifactError>(())Disabling layers
use edifact_rs::{ValidationContext, ValidationLayer};
let ctx = ValidationContext::builder()
.code_list(false) // skip code list checks
.structure(false) // skip structural checks
.build();
Builder toggles: .structure(bool), .code_list(bool), .profile(bool), .envelope(bool).
Shared Arc<ProfileRulePack> (efficient multi-context reuse)
When the same pack is used across many ValidationContext instances (e.g. in a
server hot-path), share it via Arc to avoid cloning the rule closures:
use edifact_rs::{ValidationContext, ProfileRulePack};
use std::sync::Arc;
let pack = Arc::new(
ProfileRulePack::new("ORDERS-RULES")
.for_message_type("ORDERS")
.with_stateless_rule_fn(|_segs, _issues| {}),
);
// Lightweight clone — closures are not duplicated:
let ctx1 = ValidationContext::builder()
.with_profile_pack_arc(Arc::clone(&pack))
.build();
let ctx2 = ValidationContext::builder()
.with_profile_pack_arc(Arc::clone(&pack))
.build();Early abort on first critical issue
use edifact_rs::ValidationContext;
let ctx = ValidationContext::builder()
.bail_on_first_critical(true) // stop after the first Critical-severity issue
.build();Built-in envelope validation
The EnvelopeValidator checks the whole ISO 9735-1 interchange structure —
UNB/UNZ, UNG/UNE groups, UNH/UNT messages, the control references
that tie each pair together, and the three control counts. Enable it with
with_envelope_validation():
use edifact_rs::{ValidationContext, from_bytes};
# let segs: Vec<_> = from_bytes(b"UNB+UNOA:1+SENDER:1+RECEIVER:1+200101:1000+1'UNH+1+ORDERS:D:96A:UN'BGM+220+PO-1'UNT+3+1'UNZ+1+1'").collect::<Result<_,_>>()?;
let ctx = ValidationContext::builder()
.with_envelope_validation() // adds UNB/UNG/UNH/UNT/UNE/UNZ structure checks
.build();
let report = ctx.validate_lenient(&segs);
# Ok::<(), edifact_rs::EdifactError>(())
Groups (UNG/UNE) are parsed and validated natively: nesting is rejected, the
UNE reference must repeat the UNG one, and UNZ DE 0036 is checked against
the group count rather than the message count — which is what Annex C.3.4
requires once groups are in play.
Two structural rules have no count to give them away and so are worth calling
out. An interchange with no message and no group (§7.1) raises E042, and a
message with nothing between its UNH and UNT (§7.3) raises E043. In both
cases the declared counts agree with the content, so every other check passes.
Syntax validation
SyntaxValidator checks the rules that hold for every interchange from every
partner in every directory, so it needs no configuration:
| Rule | Source | Code |
|---|---|---|
| A segment carries at least one data element besides its tag | §7.5, §8.5 | E046 |
| No data element value is made only of spaces | §9.3 | E045 (warning) |
| No segment or composite ends in a separator with nothing after it | §8.7.1, §8.7.2 | E051 (warning) |
All three are artefacts a hand-rolled writer produces by accident — an ABC'
where a conditional segment should have been dropped entirely, a fixed-width
source field copied across with its padding intact, or a loop that emits a
separator before checking whether another value follows. None trips a count
check, and all are rejected downstream.
The trailing-separator rule is narrower than it first looks. An interior
omission must keep its separator (§8.7.1 Figure 1), so BGM+220++9' is correct
and is not reported; and BGM+' is how §8.4 spells a mandatory segment with no
data to carry. Only a separator with nothing after it at all is a fault.
use edifact_rs::{ValidationContext, from_bytes};
let segments: Vec<_> = from_bytes(b"FTX+ 'DTM'").collect::<Result<Vec<_>, _>>()?;
let report = ValidationContext::builder()
.with_syntax_validation()
.build()
.validate_lenient(&segments);
assert_eq!(report.errors()[0].error_code(), Some("E046")); // DTM has no data element
assert_eq!(report.warnings()[0].error_code(), Some("E045")); // FTX value is only spaces
# Ok::<(), edifact_rs::EdifactError>(())
Note that DTM+' is not E046: an empty data element is present, which is how
EDIFACT spells a mandatory segment that has no data to carry (§8.4).
Per-message reference stamping
When validating individual messages extracted from a multi-message interchange,
use with_message_ref to stamp every emitted ValidationIssue with the UNH
reference (DE 0062). This makes it easy to map issues back to the originating
message:
use edifact_rs::{ValidationContext, from_bytes};
# let segs: Vec<_> = from_bytes(b"UNH+MSG-42+ORDERS:D:96A:UN'BGM+220+PO-1'UNT+3+MSG-42'").collect::<Result<_,_>>()?;
let ctx = ValidationContext::builder()
.with_message_type("ORDERS")
.with_message_ref("MSG-42") // DE 0062 from the UNH segment
.build();
let report = ctx.validate_lenient(&segs);
// Every issue in `report` will have `issue.message_ref == Some("MSG-42")`
# Ok::<(), edifact_rs::EdifactError>(())
validate_lenient vs validate_strict
| Method | On first error | Returns |
|---|---|---|
validate_lenient(&segs) | Continues collecting all issues | ValidationReport |
validate_strict(&segs) | Runs all validators, returns Err(report) if any Error/Critical found | Result<ValidationReport, ValidationReport> |
# use edifact_rs::{ValidationContext, from_bytes};
# let segs: Vec<_> = from_bytes(b"BGM+220+PO-4711+9'").collect::<Result<_,_>>()?;
# let ctx = ValidationContext::builder().build();
// Lenient: collect all issues even when errors are present
let report = ctx.validate_lenient(&segs);
if !report.is_valid() {
for issue in report.errors() {
eprintln!("error [{}]: {}", issue.error_code().unwrap_or("?"), issue.message);
}
for warn in report.warnings() {
eprintln!("warn: {}", warn.message);
}
}
// Strict: run all validators; get Err(report) when any Error/Critical found
match ctx.validate_strict(&segs) {
Ok(report) => println!("valid, {} warnings", report.warnings().len()),
Err(report) => {
for issue in report.errors() {
eprintln!("error [{}]: {}", issue.error_code().unwrap_or("?"), issue.message);
}
}
}
# Ok::<(), edifact_rs::EdifactError>(())
ValidationReport — working with results
# use edifact_rs::{ValidationContext, from_bytes};
# let segs: Vec<_> = from_bytes(b"BGM+220+PO-4711+9'").collect::<Result<_,_>>()?;
# let ctx = ValidationContext::builder().build();
let report = ctx.validate_lenient(&segs);
// Overall validity (no errors, no criticals)
println!("valid: {}", report.is_valid());
println!("errors: {}", report.errors().len());
println!("warnings: {}", report.warnings().len());
println!("infos: {}", report.infos().len());
println!("total issues: {}", report.total_issues());
// Deterministic string rendering (useful for snapshots / golden tests)
let text = report.render_deterministic();
println!("{text}");
// Filter by rule ID prefix (for profile-pack namespacing)
let profile_issues = report.filter_by_rule_prefix("ORDERS-DEMO-");
println!("{} ORDERS-DEMO issues", profile_issues.total_issues());
// Get issues for a specific rule (lazy iterator; collect or count as needed)
let count = report.issues_for_rule_id("ORDERS-P001").count();
println!("ORDERS-P001 findings: {count}");
# Ok::<(), edifact_rs::EdifactError>(())
ValidationIssue — building findings
Within a Validator or ProfileRulePack rule, construct ValidationIssue with the
builder API:
use edifact_rs::{Span, ValidationIssue, ValidationSeverity};
let issue = ValidationIssue::new(
ValidationSeverity::Error,
"BGM document code 999 is not accepted",
)
.with_rule_id("ORDERS-BGM-001") // stable ID for filtering / mapping
.with_segment("BGM") // which segment tag
.with_element_index(0) // which element
.with_error_code("E007") // EDIFACT or application error code
.with_suggestion("Use code 220, 231, or 261")
.with_span(Span::new(42, 60)); // byte range of the offending region| Builder method | Type | Purpose |
|---|---|---|
.with_rule_id(id) | &str | Stable ID for filtering and mapping |
.with_segment(tag) | &str | Segment tag where the issue was found |
.with_element_index(n) | usize | Element index (0-based) |
.with_error_code(code) | impl Into<Cow<'static, str>> | Stable error code string (e.g. "E007"); a &'static str costs no allocation, and the field round-trips through serde |
.with_suggestion(text) | &str | Human-friendly remediation hint |
.with_span(span) | Span | Byte range of the issue in the input — the only positional field; read issue.span.map(|s| s.start) or issue.start_offset() for the start alone |
.with_segment_occurrence(n) | u16 | Zero-based occurrence among segments with the same tag |
.with_segment_group(name) | impl Into<String> | Name of the segment group instance (e.g. "SG5") — set automatically by group-scoped rules |
.with_message_ref(r) | impl Into<String> | UNH reference (DE 0062) — usually set automatically via ValidationContextBuilder::with_message_ref |
.with_context_entry(k, v) | (impl Into<String>, impl Into<String>) | Insert a single key-value pair into the domain metadata map |
.with_context_entries(iter) | impl IntoIterator<Item=(K,V)> | Bulk-insert domain metadata; duplicate keys overwrite |
Rule ID prefix convention
rule_id doubles as a lightweight metadata carrier. Use a structured,
namespaced prefix so downstream code can extract domain identifiers without
parsing the human-readable message:
"<PACK>-<SCOPE>-<TAG>-<STATUS>"
^^^^^^ — the pack / profile that owns the rule
^^^^^^^ — a process identifier, group name, or other discriminator
^^^^^ — the affected segment
^^^^^^^^ — M / C / … status or short discriminator
Example: "PROFILE-4711-BGM-M" — pack PROFILE, scope 4711, segment BGM,
mandatory (M). Recover the scope:
let rule_id = "PROFILE-4711-BGM-M";
let scope = rule_id.strip_prefix("PROFILE-").and_then(|s| s.split('-').next());
assert_eq!(scope, Some("4711"));
For arbitrary domain metadata use with_context_entry instead:
use edifact_rs::{Span, ValidationIssue, ValidationSeverity};
let issue = ValidationIssue::new(ValidationSeverity::Error, "BGM code invalid")
.with_rule_id("PROFILE-4711-BGM-M")
.with_context_entry("pid", "4711")
.with_context_entry("partner", "9900123456789");
assert_eq!(issue.context_get("pid"), Some("4711"));
Custom validator implementations
For complex validation that needs shared state across segments (e.g. reference
counting, cross-segment consistency), implement Validator as a struct:
use edifact_rs::{Validator, ValidationReport, ValidationRuleContext, ValidationIssue, ValidationSeverity, Segment};
struct ReferenceConsistencyValidator;
impl Validator for ReferenceConsistencyValidator {
fn validate_batch(
&self,
segments: &[Segment<'_>],
report: &mut ValidationReport,
_context: &ValidationRuleContext<'_>,
) {
// Find the UNH reference
let unh_ref = segments
.iter()
.find(|s| s.tag == "UNH")
.and_then(|s| s.element_str(0))
.unwrap_or("");
// Find the UNT reference
let unt_ref = segments
.iter()
.find(|s| s.tag == "UNT")
.and_then(|s| s.element_str(1))
.unwrap_or("");
if unh_ref != unt_ref {
report.add_error(
ValidationIssue::new(
ValidationSeverity::Error,
format!("UNH reference '{unh_ref}' does not match UNT reference '{unt_ref}'"),
)
.with_rule_id("ENVELOPE-REF-PARITY")
.with_segment("UNT")
.with_element_index(1),
);
}
}
}
See cookbook_fixture_validation.rs
for a complete validator with fixture-based test data.
Progressive streaming validation
Validate each UNH..UNT window as it arrives from the reader:
use edifact_rs::{
ValidationContext, ProfileRulePack, ValidationIssue, ValidationSeverity,
message_windows_from_reader,
};
use std::io::Cursor;
let input = Cursor::new(b"\
UNH+1+ORDERS:D:11A:UN'BGM+220+PO-001+9'UNT+3+1'\
UNH+2+ORDERS:D:11A:UN'BGM+220+PO-002+9'UNT+3+2'".to_vec());
let ctx = ValidationContext::builder()
.with_profile_pack(
ProfileRulePack::new("ORDERS-REQUIRED")
.for_message_type("ORDERS")
.with_stateless_rule_fn(|segs, issues| {
if !segs.iter().any(|s| s.tag == "BGM") {
issues.push(
ValidationIssue::new(ValidationSeverity::Error, "BGM is required")
.with_rule_id("ORDERS-REQ-BGM"),
);
}
}),
)
.build();
for result in message_windows_from_reader(input) {
let window = result?;
let borrowed: Vec<_> = window.segments.iter().map(|s| s.as_borrowed()).collect();
let report = ctx.validate_lenient(&borrowed);
println!(
"message {:?}: {} error(s)",
window.message_type,
report.errors().len()
);
}
# Ok::<(), edifact_rs::EdifactError>(())
See cookbook_streamed_progressive_validation.rs
for a complete example.
Group-aware validation
Group-aware validation fires ProfileRulePack group rules once per segment-group
occurrence (e.g. once per SG5 instance) rather than once across the entire
message. First define a [GroupDef] schema, build a SegmentGroupIndexed tree
with group_segments_indexed, then pass the tree to validate_lenient_grouped:
use edifact_rs::{
ValidationContext, ProfileRulePack,
group::{GroupDef, group_segments_indexed},
from_bytes,
};
// Schema: SG5 starts at LIN and contains an SG6 sub-group starting at QTY.
static SG6: &[GroupDef] = &[GroupDef::new("SG6", "QTY")];
static SCHEMA: &[GroupDef] = &[GroupDef::with_children("SG5", "LIN", SG6)];
let segs: Vec<_> = from_bytes(
b"UNH+1+ORDERS:D:96A:UN'\
LIN+1'QTY+21:10'\
LIN+2'\
UNT+5+1'"
).collect::<Result<_, _>>()?;
// Build the indexed group tree (O(n × schema_depth), no segment clones):
let tree = group_segments_indexed(&segs, SCHEMA, "ROOT");
let pack = ProfileRulePack::new("ORDERS")
.for_message_type("ORDERS")
// Require QTY inside every SG5 occurrence:
.require_segment_in_group("SG5", "QTY", "ORDERS-SG5-QTY-M");
let ctx = ValidationContext::builder()
.with_profile_pack(pack)
.build();
// validate_lenient_grouped runs the flat pass then the group pass:
let report = ctx.validate_lenient_grouped(&tree, &segs);
println!("{} error(s)", report.errors().len());
# Ok::<(), edifact_rs::EdifactError>(())
For owned segments (e.g. from message_windows_from_reader), use the _owned
variants:
| Method | Args | Segment type | Mode |
|---|---|---|---|
validate_lenient_grouped(root, segs) | (&SegmentGroupIndexed, &[Segment]) | borrowed | Collect all issues |
validate_strict_grouped(root, segs) | (&SegmentGroupIndexed, &[Segment]) | borrowed | Err on first error/critical |
validate_lenient_grouped_owned(root, segs) | (&SegmentGroupIndexed, &[OwnedSegment]) | owned | Collect all issues |
validate_strict_grouped_owned(root, segs) | (&SegmentGroupIndexed, &[OwnedSegment]) | owned | Err on first error/critical |
Schemas built at runtime
GroupDef<'a> borrows its names and its child slice, so a static table is
GroupDef<'static> and costs nothing, while a schema read out of a MIG at
startup borrows from storage you own. Both go through the same function:
use edifact_rs::group::{GroupDef, group_segments_indexed};
use edifact_rs::from_bytes;
// Stand-in for names read out of a MIG file at startup.
let names: Vec<String> = vec!["SG5".into(), "LIN".into()];
let schema = vec![GroupDef::new(&names[0], &names[1])];
let segs: Vec<_> = from_bytes(b"UNH+1+ORDERS:D:96A:UN'LIN+1'UNT+3+1'")
.collect::<Result<_, _>>()?;
let tree = group_segments_indexed(&segs, &schema, "ROOT");
assert_eq!(tree.children[0].definition, "SG5");
# Ok::<(), edifact_rs::EdifactError>(())What a group actually spans
Grouping is driven purely by trigger tags: a group runs from its trigger up to
the next trigger of a sibling or ancestor, or to the end of the slice. Nothing
stops the last group of a message at UNT, so the trailer lands inside whichever
group ran last.
MessageWindow::segments deliberately includes UNH and UNT so that
envelope-aware consumers can read them; pass MessageWindow::body() — which is
exactly the segments between them — when the group boundaries matter.
See Profile Packs — Group-scoped rules for how to build group rules.
Directory validator
DirectoryValidator provides structural validation against a user-supplied segment
definition dictionary:
use edifact_rs::{DirectoryValidatorBuilder, OwnedSegmentDef, OwnedElementRef, Status};
let validator = DirectoryValidatorBuilder::new("CUSTOM-D96A")
.add_segment(
OwnedSegmentDef::new_unchecked(
"BGM".to_owned(),
"Beginning of message".to_owned(),
vec![
OwnedElementRef::new_unchecked(1, "1001".to_owned(), Status::Conditional, 1),
OwnedElementRef::new_unchecked(2, "1004".to_owned(), Status::Conditional, 1),
OwnedElementRef::new_unchecked(3, "1225".to_owned(), Status::Conditional, 1),
],
),
)
.build();
Declaring a composite's components with ElementRef::composite (or
OwnedElementRef::with_components) does three things: it activates the
mandatory-component check, it caps the composite's arity (more components
than the directory declares is E013; fewer is normal, since conditional
components may be omitted), and it makes the composite's contents reachable by
identifier — see below. Definitions that declare no components behave exactly as
before, so this is inert for any directory table that has not opted in.
An expected_components hook, when set, still wins for that element: it is an
exact count, whereas declared components are an upper bound.
Scope note:
DirectoryValidatorvalidates element presence and length within individual segments. It does not enforce full EDIFACT message grammar (conditional segment groups, repeat counts). UseProfileRulePackfor those cross-segment rules.
Data element representations
Every UN/EDIFACT directory prints a representation beside each data element, and
it is what partners actually reject on: a sender identification of 40 characters
where the standard says an..35 is refused at the far end, long after it was
sent. Attach it with with_repr and DirectoryValidator checks it:
use edifact_rs::{ComponentRef, ElementRef, Repr, SegmentDefinition, Status};
static C507: &[ComponentRef] = &[
ComponentRef::new(1, "2005", Status::Mandatory).with_repr(Repr::an_up_to(3)),
ComponentRef::new(2, "2380", Status::Conditional).with_repr(Repr::an_up_to(35)),
ComponentRef::new(3, "2379", Status::Conditional).with_repr(Repr::an_up_to(3)),
];
static DTM_ELEMENTS: &[ElementRef] =
&[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
static DTM: SegmentDefinition = SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
Three things follow, and each maps onto a CONTRL code:
| Violation | Code | CONTRL |
|---|---|---|
| Wrong character class | E048 | 37 |
| Longer than the maximum | E049 | 39 |
| Shorter than a fixed length | E050 | 40 |
Positions with no declared representation are simply not checked, so a partial table stays useful instead of becoming a source of false findings.
Length is counted in characters
ISO 9735-1 §6: "one graphic character shall be counted as one character,
irrespective of the number of bytes/octets required to encode it." A UNOC ü
is one character, not the two bytes UTF-8 needs for it. The release character is
excluded too (§5), which is automatic here because release sequences are resolved
before validation.
A numeric value excludes more. §10: "the length of a numeric data element
value shall not include the minus sign (-), the decimal mark (. or ,), or the
exponent mark (E or e) and its exponent" — so -123.45 is five characters and
fits n..5.
What n admits
§10 takes ISO 6093's forms and subtracts: "The space character and plus sign
shall not be allowed", and "when a decimal mark is transferred, there shall be at
least one digit after the decimal mark". So 2, 2.00, 0.5, .5 and -3E4
are numeric; 1., ., +1 and 1 2 are not.
The shipped service tables carry theirs
edifact_rs::service declares the representations from ISO 9735-1 Annex C, so
UNZ+abc+IC1' (DE 0036 is n..6) and a 20-character UNB DE 0020 (an..14)
are rejected with no directory involved.
The one position that changed between syntax versions
S004 DE 0017, the date of preparation, is the only place in the service
directory where version 4 is not a superset of version 3. Version 3 transfers
YYMMDD (n6); version 4 widened it to CCYYMMDD (n8) to be year-2000
correct.
Collapsing the two into n..8 would validate neither version: a six-digit date
would pass in a version 4 interchange, and a seven-digit one in either. So the
table declares both, and the checker picks by the version in UNB S001 DE 0002:
use edifact_rs::{ComponentRef, Repr, Status};
const DATE: ComponentRef = ComponentRef::new(1, "0017", Status::Mandatory)
.with_repr_by_syntax_version(Repr::n(6), Repr::n(8));
UNB+UNOA:3+…+200101:0900+… passes and …+20200101:0900+… is rejected; under
UNOC:4 it is the other way round.
When the version cannot be determined — validating a bare message window, which
carries no UNB — both forms are accepted, because guessing would reject
conformant data from whichever version was guessed against. A length that is
neither, such as seven digits, is still rejected.
Suppression of insignificant characters
With a representation attached, DirectoryValidator also applies §9.1: leading
zeroes must be suppressed in a variable-length numeric value, trailing spaces
in a variable-length text one. Both raise
E053 as a warning.
Two exemptions come straight from the clause. "Nevertheless, a single zero before
a decimal mark is allowed", so 0.5 passes and 00.5 does not. And a
fixed-length element is exempt entirely — n3 is zero-padded by design.
Occurrence limits
ElementRef's max_repeat is enforced as well, raising
E047 (CONTRL 35) when a
repeating data element occurs more often than the definition allows.
Auditing a hand-written layout
Authoring a SegmentDefinition by hand has a silent failure mode. A layout that
disagrees with the wire resolves value_by_code to the wrong component,
returns a plausible value, and every test still passes — the definition is the
only thing in the program that says what the positions mean, so nothing inside
the program can contradict it.
SegmentLayout::audit breaks that circle by pointing the definition at real
messages:
use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status, from_bytes};
// A hand-authored C507 that stops one component short of the directory.
static C507: &[ComponentRef] = &[
ComponentRef::new(1, "2005", Status::Mandatory),
ComponentRef::new(2, "2380", Status::Conditional),
];
static DTM_ELEMENTS: &[ElementRef] =
&[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
static DTM: SegmentDefinition = SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
let corpus: Vec<_> = from_bytes(b"DTM+137:20260101:102'").collect::<Result<Vec<_>, _>>()?;
let audit = DTM.audit(&corpus);
// The format qualifier `102` has nowhere to go — the layout is short.
assert!(audit.has_contradictions());
# Ok::<(), edifact_rs::EdifactError>(())
Three kinds of finding come back, and the distinction between them is the point:
| Finding | Means |
|---|---|
UndeclaredElement / UndeclaredComponent | The wire carries a value the layout has no slot for — the layout is wrong. |
MandatoryNeverPopulated | A slot the layout calls mandatory is empty everywhere — the status or the position is wrong. |
NeverObserved | Nothing in the corpus reaches this slot — the corpus cannot confirm it. |
NeverObserved is not a defect, and has_contradictions deliberately excludes
it. It is the honest answer to "does my definition match the directory?" when the
fixtures are too thin to say, and it names exactly which positions still need a
human to check them against the directory — or a fixture that reaches them:
# use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status, from_bytes};
# static C507: &[ComponentRef] = &[
# ComponentRef::new(1, "2005", Status::Mandatory),
# ComponentRef::new(2, "2380", Status::Conditional),
# ComponentRef::new(3, "2379", Status::Conditional),
# ];
# static DTM_ELEMENTS: &[ElementRef] =
# &[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
# static DTM: SegmentDefinition = SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
// No fixture in this corpus carries a format qualifier.
let corpus: Vec<_> = from_bytes(b"DTM+137:20260101'").collect::<Result<Vec<_>, _>>()?;
let audit = DTM.audit(&corpus);
assert!(!audit.has_contradictions()); // nothing is disproved …
let pending: Vec<&str> = audit.unconfirmed().map(|s| s.data_element.as_str()).collect();
assert_eq!(pending, ["2379"]); // … but 2379 is still unverified
# Ok::<(), edifact_rs::EdifactError>(())
The assertion to put in a test is !audit.has_contradictions(), together with
audit.segments_examined() > 0 — an audit over a corpus with none of the segment
proves nothing, and a green test that silently proved nothing is the failure this
whole facility exists to prevent. LayoutAudit also implements Display, so
printing it gives a report you can paste into a review.
Findings are deduplicated per position, so a corpus of hundreds of fixtures reports each disagreement once rather than once per message.
Auditing a whole directory
audit_directory answers the question a hand-authored directory actually raises
— which of my definitions does this corpus disprove, and which can it not speak
to? — in one call:
use edifact_rs::{audit_directory, from_bytes, service};
let corpus: Vec<_> = from_bytes(
b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'UNT+2+M1'UNZ+1+IC1'",
)
.collect::<Result<Vec<_>, _>>()?;
for audit in audit_directory(service::lookup, &corpus) {
assert!(!audit.has_contradictions(), "{audit}");
}
# Ok::<(), edifact_rs::EdifactError>(())
Only tags the corpus contains are audited. A definition the fixtures never
exercise would produce nothing but NeverObserved entries and bury the findings
that matter — ask SegmentLayout::audit directly for those.
Mandatory components of an absent composite
ISO 9735-1 §8.6 makes a mandatory component required "if the composite data
element is present", not unconditionally — and the audit follows that. UNB S005
component 1 (DE 0022, the recipient password) is mandatory inside a composite
that is itself conditional, so a conformant UNB with no password does not
violate its own layout. It comes back as NeverObserved instead, which is the
truthful answer: the corpus has nothing to say about it.
Once the composite is present, its mandatory components are required again, and a missing one is a contradiction.
Code-addressed element access
Positional accessors (seg.element_str(4), seg.component_str(1, 2)) address
data by index. A transposed index reads the wrong data element and still
validates clean — silently. Given a SegmentLayout — implemented by both
SegmentDefinition (compile-time tables) and OwnedSegmentDef (runtime-loaded
definitions) — the same data can be addressed by its UN/EDIFACT identifier
instead, and a wrong reference becomes a directory 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 segments: Vec<_> = from_bytes(b"NAD+MS+9900112233445::293'").collect::<Result<Vec<_>, _>>()?;
let nad = &segments[0];
assert_eq!(nad.value_by_code(&NAD, "3039")?, Some("9900112233445"));
assert_eq!(nad.value_by_code(&NAD, "3055")?, Some("293"));
// DE 2380 belongs to DTM, not NAD — an error, not a wrong-but-quiet read.
assert!(nad.value_by_code(&NAD, "2380").is_err());
# Ok::<(), edifact_rs::EdifactError>(())| Method | On | Returns |
|---|---|---|
value_by_code(layout, de) | Segment, BorrowedSegment, OwnedSegment | Result<Option<&str>, EdifactError> |
span_by_code(layout, de) | Segment, BorrowedSegment, OwnedSegment | Result<Option<Span>, EdifactError> — attach to a ValidationIssue with with_span |
element_by_code(layout, de) | Segment, BorrowedSegment, OwnedSegment | Result<Option<Element>, EdifactError> — the enclosing composite when de names a component |
SegmentLayout::resolve_code(de) | SegmentDefinition, OwnedSegmentDef | Result<ElementPath, EdifactError> — resolve once, then read many segments with value_at / span_at |
Three distinct failures are reported rather than silently tolerated:
UnknownDataElement (E033), AmbiguousDataElement (E034) when a directory
repeats a code, and SegmentLayoutMismatch (E035) when a layout is applied to
a segment with a different tag.
The derive has the same addressing under #[edifact(layout = ...)], where
resolution happens at compile time — see
Typed Derive.
Next steps
- Profile Packs — composable business-rule bundles
- Error Reference — stable codes for
ValidationIssue::error_code - Diagnostics — human-readable error rendering