Validation

The Validator trait, ValidationContext, and the layered envelope / structure / code-list / profile validation pipeline.

On this page
  1. Key types
  2. The Validator trait
  3. ValidationContext and layers
    1. Disabling layers
    2. Shared Arc<ProfileRulePack> (efficient multi-context reuse)
    3. Early abort on first critical issue
    4. Built-in envelope validation
    5. Syntax validation
    6. Per-message reference stamping
  4. validate_lenient vs validate_strict
  5. ValidationReport — working with results
  6. ValidationIssue — building findings
    1. Rule ID prefix convention
  7. Custom validator implementations
  8. Progressive streaming validation
  9. Group-aware validation
    1. Schemas built at runtime
    2. What a group actually spans
  10. Directory validator
  11. Data element representations
    1. Length is counted in characters
    2. What n admits
    3. The shipped service tables carry theirs
    4. The one position that changed between syntax versions
    5. Suppression of insignificant characters
    6. Occurrence limits
  12. Auditing a hand-written layout
    1. Auditing a whole directory
    2. Mandatory components of an absent composite
  13. Code-addressed element access
  14. Next steps

edifact-rs provides a layered, composable validation pipeline that separates structural, code-list, and profile-level checks — each pluggable independently.


Key types

TypeRole
ValidatorTrait — implement to create a custom validator
ValidationContextOrchestrates multiple validators across layers
ValidationLayerEnum — Structure, CodeList, Profile
ValidationReportAggregated result — errors, warnings, infos
ValidationIssueA single finding — severity, message, rule ID, offsets
ValidationSeverityCritical, Error, Warning, Info
ProfileRulePackComposable bundle of closure-based profile rules
validate_eachHelper — 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:

  1. Structure — check mandatory segments, ordering, counts
  2. CodeList — check element values against UNTDID code lists
  3. Profile — 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:

RuleSourceCode
A segment carries at least one data element besides its tag§7.5, §8.5E046
No data element value is made only of spaces§9.3E045 (warning)
No segment or composite ends in a separator with nothing after it§8.7.1, §8.7.2E051 (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

MethodOn first errorReturns
validate_lenient(&segs)Continues collecting all issuesValidationReport
validate_strict(&segs)Runs all validators, returns Err(report) if any Error/Critical foundResult<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 methodTypePurpose
.with_rule_id(id)&strStable ID for filtering and mapping
.with_segment(tag)&strSegment tag where the issue was found
.with_element_index(n)usizeElement 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)&strHuman-friendly remediation hint
.with_span(span)SpanByte 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)u16Zero-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:

MethodArgsSegment typeMode
validate_lenient_grouped(root, segs)(&SegmentGroupIndexed, &[Segment])borrowedCollect all issues
validate_strict_grouped(root, segs)(&SegmentGroupIndexed, &[Segment])borrowedErr on first error/critical
validate_lenient_grouped_owned(root, segs)(&SegmentGroupIndexed, &[OwnedSegment])ownedCollect all issues
validate_strict_grouped_owned(root, segs)(&SegmentGroupIndexed, &[OwnedSegment])ownedErr 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: DirectoryValidator validates element presence and length within individual segments. It does not enforce full EDIFACT message grammar (conditional segment groups, repeat counts). Use ProfileRulePack for 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:

ViolationCodeCONTRL
Wrong character classE04837
Longer than the maximumE04939
Shorter than a fixed lengthE05040

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 UNBboth 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:

FindingMeans
UndeclaredElement / UndeclaredComponentThe wire carries a value the layout has no slot for — the layout is wrong.
MandatoryNeverPopulatedA slot the layout calls mandatory is empty everywhere — the status or the position is wrong.
NeverObservedNothing 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>(())
MethodOnReturns
value_by_code(layout, de)Segment, BorrowedSegment, OwnedSegmentResult<Option<&str>, EdifactError>
span_by_code(layout, de)Segment, BorrowedSegment, OwnedSegmentResult<Option<Span>, EdifactError> — attach to a ValidationIssue with with_span
element_by_code(layout, de)Segment, BorrowedSegment, OwnedSegmentResult<Option<Element>, EdifactError> — the enclosing composite when de names a component
SegmentLayout::resolve_code(de)SegmentDefinition, OwnedSegmentDefResult<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