Platform

Use Platform for multi-tenant gateways, test isolation, hot-reload, and custom DoS limits. Alternative to the global ReleaseRegistry singleton.

Platform Guide

The Platform struct provides explicit, isolated instances of the EDI@Energy processing pipeline. This is the recommended approach for multi-tenant servers, integration tests, and any application that needs more than one profile configuration at the same time.


Why Platform?

The top-level parse() and parse_interchange() functions use ReleaseRegistry::global() — a process-wide singleton initialized on first use. This is fine for simple command-line tools and single-tenant services.

Platform is better when you need:

NeedProblem with globalsSolution
Test isolationConcurrent tests that register custom profiles interfere with each otherEach test gets its own Platform
Multi-tenant gatewaysStrom and Gas tenants need different profile subsetsOne Platform per tenant
Hot-reloadNew BDEW release requires a process restartSwap Arc<Platform> at runtime
Custom DoS limitsGlobal defaults may be too generous or too strictplatform.parse_with_config(bytes, config)

Basic Usage

use edi_energy::Platform;

// Create a platform with all built-in profiles enabled
let platform = Platform::with_all_profiles();

let input = std::fs::read("message.edi")?;
let msg = platform.parse(&input)?;
let report = msg.validate()?;

Custom Profile Subset

Platform::with_all_profiles() is the supported way to obtain a platform backed by the crate's built-in profiles. Each call builds a fresh, independent ReleaseRegistry:

use edi_energy::Platform;

let platform = Platform::with_all_profiles();
let msg = platform.parse(bytes)?;

There is no public API to build a registry containing an arbitrary subset of the built-in profiles at runtime. The generated per-profile statics and the profile-registration entry point are crate-private and are not re-exported. The edi_energy::releases module exposes &'static Release identifiers (e.g. edi_energy::releases::mscons_fv20261001() returns a &'static Release), not Profile objects, so they cannot be handed to a registry constructor.

To trim which built-in profiles are compiled in, use the crate's per-message-type Cargo features (utilmd, mscons, aperak, invoic, …). A profile that is not enabled at build time is simply absent from with_all_profiles().

The one runtime constructor for a custom registry is ReleaseRegistry::new(Vec<&'static dyn Profile>) combined with Platform::new(registry). This path is intended for callers that supply their own Profile implementations — for example, hand-written profiles for classic 5.5.x archive releases that the crate does not bundle:

use edi_energy::{Platform, registry::{Profile, ReleaseRegistry}};

// `my_profiles::register` pushes your own `&'static dyn Profile` implementations.
let mut profiles: Vec<&'static dyn Profile> = Vec::new();
my_profiles::register(&mut profiles);

let platform = Platform::new(ReleaseRegistry::new(profiles));
let msg = platform.parse(bytes)?;

You can also widen the receive tolerance on any platform:

use edi_energy::Platform;

// The BDEW default is 0 — EDIFACT changes format at a single instant.
// Raise it for a tenant whose contract tolerates a late-arriving old-format message.
let platform = Platform::with_all_profiles().with_receive_tolerance_days(3);

Custom ParseConfig

Platform does not store a ParseConfig. Instead, pass a config per call via parse_with_config. ParseConfig exposes its DoS limits as public fields, so build one from ParseConfig::default() with struct-update syntax:

use edi_energy::{Platform, ParseConfig};

let config = ParseConfig {
    max_input_bytes: Some(512_000), // 512 KB
    max_segments: Some(1_000),
    ..ParseConfig::default()
};

let platform = Platform::with_all_profiles();
let msg = platform.parse_with_config(bytes, config)?;

The interchange API takes a config the same way via Platform::parse_interchange_with_config(reader, config).

The only builder-style method on ParseConfig is with_reference_date, which pins the date used for profile validity lookups during validation (useful for deterministic tests):

use edi_energy::ParseConfig;

let config = ParseConfig::default().with_reference_date(
    time::Date::from_calendar_date(2026, time::Month::January, 1).unwrap(),
);

Test Isolation

The most important use case. Instead of depending on the global registry, give each test its own platform:

#[test]
fn my_utilmd_test() {
    let platform = Platform::with_all_profiles();
    let msg = platform.parse(UTILMD_BYTES).unwrap();
    let report = msg.validate().unwrap();
    assert!(report.is_valid());
}

#[test]
fn my_mscons_test() {
    // Independent — does not share state with my_utilmd_test
    let platform = Platform::with_all_profiles();
    let msg = platform.parse(MSCONS_BYTES).unwrap();
    assert_eq!(msg.try_message_type().map(|t| t.as_str()), Some("MSCONS"));
}

Platforms are cheap to create (profiles use LazyLock internally so rule-pack construction is amortized).


Sharing Platforms (Arc)

Platforms implement Clone via Arc<ReleaseRegistry> sharing — the underlying profile data is not duplicated:

use std::sync::Arc;
use edi_energy::Platform;

let shared = Arc::new(Platform::with_all_profiles());

// Hand clones to worker threads
let worker_platform = shared.clone();
std::thread::spawn(move || {
    let msg = worker_platform.parse(bytes).unwrap();
    // ...
});

ReleaseRegistry Deep Dive

ReleaseRegistry maps (message_type_code, association_code) pairs to Arc<dyn Profile> objects. Each profile bundles:

  • MIG rule pack — segment structure rules
  • AHB rule packs — per-PID validation rules
  • Codelists — allowed values per data element
  • Metadatavalid_from, valid_until, source_document

The registry resolves the correct profile using the UNH association code (DE 0057) extracted from each parsed message.

Format boundaries

EDIFACT has no transition window. Allgemeine Festlegungen 6.1 §2.5 gives the EDIFACT formats a single Anwendungszeitpunkt — 1 April or 1 October — with no overlap around it: before that instant the old format applies, from it the new one does.

The 15-Werktage Übergangszeitraum that does exist is §8.5, and it is the XML rule. It starts at the Anwendungszeitpunkt rather than before it, runs in Werktage rather than calendar days, and picks the version by the Erfüllungsdatum in the message rather than by when it was sent. None of that carries over.

  • is_acceptable_on accepts a release from its valid_from through its valid_until — the leading edge is hard.
  • ReleaseRegistry::with_receive_tolerance_days(n) extends the trailing edge by n calendar days, for operators who choose to accept a late-arriving message in the superseded format. It is a local receiving policy, not a BDEW rule, and it defaults to DEFAULT_RECEIVE_TOLERANCE_DAYS = 0.
  • TransitionState::Transition therefore occurs only with a non-zero tolerance, and only after the boundary.
  • ParseConfig::with_reference_date() pins the date profile resolution uses, so a test can sit on any boundary deterministically.

Interchange Parsing

Platform exposes the same interchange API as the free functions:

use std::io::BufReader;
use std::fs::File;

let file = File::open("bulk.edi")?;
let reader = BufReader::new(file);

for result in platform.parse_interchange(reader) {
    let msg = result?;
    if let Some(mt) = msg.try_message_type() { println!("{}", mt.as_str()); }
}

See Also

Edit this page ↗