rubo4e
Rust implementation of the BO4E energy-market data standard — the canonical data model for the German energy industry.
Not an official BO4E implementation. The reference implementation is BO4E-python. This crate aims for idiomatic Rust ergonomics, strong domain types, and ecosystem integration.
Features
- Generated types from the official BO4E JSON Schema (v202607)
- Strong domain identifiers —
MaloId,MeloId,EicCode,ObisCode,MarktpartnerId, … with embedded validation and domain helpers - Three-layer validation — constructor checks,
gardestruct rules, cross-field business logic - Typed builders — compile-time required-field enforcement via
typed-builder; optional-field setters accept bothTandOption<T> - German / snake_case / canonical JSON — BO4E wire format out of the box
- Ergonomic convenience API — extension traits, billing-period helpers, EDIFACT agency codes
- JSON Schema via
schemars, OpenAPI viautoipa, PostgreSQL viasqlx - Golden corpus and fuzz harnesses included; proptest round-trip tests run as dev tests
Installation
[]
= "0.6"
Enable optional features as needed:
= { = "0.6", = ["versioned", "time", "decimal", "json", "validate"] }
Quick Start
use *; // identifiers, BetragExt, MengeExt, PreisExt, Bo4eJsonExt
use ;
Feature Gates
| Feature | Default | Description |
|---|---|---|
identifiers |
✓ | Identifier types (MaloId, EicCode, ObisCode, …) + serde — zero schema overhead |
serde |
✓ | Serde derives + extension-data map |
json |
serde_json helpers (to_json_german(), …) |
|
simd-json |
SIMD-accelerated JSON parsing backend | |
time |
time crate — Date for date fields, OffsetDateTime for timestamps |
|
decimal |
rust_decimal::Decimal for amounts and prices |
|
builder |
typed-builder derives on all BO/COM structs |
|
validate |
garde validation — constructor + cross-field rules |
|
schemars |
JSON Schema generation with patterns and examples | |
sqlx |
Type/Encode/Decode for all identifier types (PostgreSQL) |
|
utoipa |
ToSchema with pattern/example/description for OpenAPI |
|
strum |
Enum iteration and string conversion | |
versioned |
Versioned schema modules (v202607, current) |
|
tracing |
Structured diagnostics via the tracing crate |
|
metrics |
Counter export hooks (metrics ecosystem) |
Typical full setup:
= { = "0.6", = ["versioned", "time", "decimal", "json", "validate", "builder"] }
Schema Versions
| Module | Schema tag | Status |
|---|---|---|
v202607 |
v202607.0.0 | Current stable |
use Marktlokation; // pin to v202607
use Marktlokation; // always the latest stable — advances with crate updates
Identifiers
All domain identifiers validate their format at construction time. There are no panicking constructors.
| Type | Format / Rule |
|---|---|
MaloId |
11 digits, BDEW alternating-weight check digit |
NeloId |
11 chars: Codetyp 'E' + 9 [A-Z0-9] + ASCII-Verfahren check digit (BDEW §4.2) |
SrId |
11 chars: Codetyp 'C' + 9 [A-Z0-9] + ASCII-Verfahren check digit (BDEW §6.6) |
TrId |
11 chars: Codetyp 'D' + 9 [A-Z0-9] + ASCII-Verfahren check digit (BDEW §6.6) |
MeloId |
33 chars: 2-char ISO country code + 31 alphanumeric |
EicCode |
16-char EIC with ENTSO-E check character |
ObisCode |
[A-B:]C.D[.E][*F]; C=0 permitted (IEC 62056-61 general metering group) |
MarktpartnerId |
13 decimal digits — BDEW (prefix 99), DVGW (prefix 98), or GS1 GLN |
// Build from base (check digit computed automatically)
let malo = from_base?; // → "51238696780"
let c = check_digit?; // → 0u8
// NeloId / SrId / TrId — same from_base pattern
let nelo = from_base?; // → "E0000000019" (ASCII-Verfahren check)
let sr = from_base?; // → "C0000000003"
let tr = from_base?; // → "D0000000002"
// Country code extraction (MeloId)
let melo = new?;
assert_eq!;
assert!;
// EDIFACT agency codes (MarktpartnerId) — eliminates duplicate mapping tables
let mp = new?;
assert!;
assert_eq!; // EDIFACT NAD DE3055
assert_eq!; // EDIFACT UNB DE0007
// Integer round-trip for legacy systems
assert_eq!;
// Serde as integer (opt-in, field-level)
pub partner_id: MarktpartnerId,
OBIS codes (EDIFACT support)
// Standard OBIS codes
let obis = new?; // active energy total
let obis = new?; // C=0 — general metering group (IEC 62056-61)
// F separator normalisation — & is accepted and stored as *
assert_eq!;
// Structured accessors
assert_eq!; // F stripped
assert_eq!; // F kept
Multi-version Dispatch (F4)
When a storage layer (e.g. PostgreSQL JSONB) writes a bo4e_version column alongside
BO4E JSON, the idiomatic dispatch pattern is a plain match:
use ;
This pattern:
- Requires no new rubo4e API —
schema_version()is already on every BO type viaBo4eObject - Is trivially extensible: each new schema version is one
matcharm - Localises migration to the storage layer; business logic only handles the current version
- Avoids over-engineering (
traitobjects,Any*enums) for a straightforward branch
See docs/versioning.md for the full upgrade workflow.
Convenience API
Extension traits — flatten Option<Com> to Option<Decimal>
use *; // brings BetragExt, MengeExt, PreisExt into scope
// Before (v0.3 — two levels of unwrap):
let net = pos.gesamtpreis.as_ref.and_then;
// After (v0.4):
let net = pos.gesamtpreis.wert_decimal; // Option<Decimal> via BetragExt
let qty = pos.positions_menge.wert_decimal; // Option<Decimal> via MengeExt
let unit = pos.einzelpreis.wert_decimal; // Option<Decimal> via PreisExt
Billing and validity helpers
use ;
use date;
// Rechnung — closed billing period
if let Some = rechnung.billing_period
// Navigate rechnungsperiode fields directly
let start: = rechnung.period_start;
let end: = rechnung.period_end;
// Iterate line items
for pos in rechnung.positions
// Decimal totals — direct access
let net = rechnung.gesamtnetto_decimal; // Option<Decimal>
let tax = rechnung.gesamtsteuer_decimal; // Option<Decimal>
let gross = rechnung.gesamtbrutto_decimal; // Option<Decimal>
let pay = rechnung.zu_zahlen_decimal; // Option<Decimal> — final amount due
let disc = rechnung.rabatt_netto_decimal; // Option<Decimal> — net discount
let next = rechnung.zukuenftiger_abschlag_decimal; // Option<Decimal>
let adv = rechnung.vorauszahlungen_summe; // Option<Decimal> — sum of advance payments
// Invoice flags — unwrap_or(false), no Option juggling
if rechnung.is_storno
if rechnung.is_original
// Date fields
let due: = rechnung.faelligkeitsdatum_date;
// Rechnungsposition — delivery period from embedded Zeitraum
let von: = pos.lieferung_von_date; // reads lieferungszeitraum.startdatum
let bis: = pos.lieferung_bis_date; // reads lieferungszeitraum.enddatum
let in_period: bool = pos.lieferungszeitraum_contains;
// PreisblattNetznutzung — point-in-time validity check
let valid = preisblatt.is_valid_at;
// Zeitraum — open/closed range helpers
let closed = z.as_closed_range; // Option<(Date, Date)>
let half_open = z.as_half_open_range; // Option<(Date, Option<Date>)>
let contains = z.contains; // bool — [start, end) half-open
JSON Handling
use Bo4eJsonExt;
use Marktlokation;
let malo: Marktlokation = todo!;
// Serialize
let german = malo.to_json_german?; // {"marktlokationsId":"…","sparte":"…",…}
let snake_case = malo.to_json_snake_case?; // {"marktlokations_id":"…","sparte":"…",…}
let canonical = malo.to_json_canonical?; // sorted keys, stable for hashing/signing
// Deserialize
let restored = from_json_german?;
Unknown JSON fields are preserved through round-trips via the _additional
extension-data map (requires json feature). This allows forward-compatible
handling of new BO4E fields without library updates.
Validation
use Validate as _;
use Validated;
use Marktlokation;
// Direct validation — returns garde::Report on failure
let malo: Marktlokation = todo!;
malo.validate?;
// Type-safe wrapper — only constructible via validation
let validated = new?; // Err(garde::Report) if invalid
let inner: &Marktlokation = &validated; // Deref to inner type
Cross-field rules (e.g. exactly one of lokationsadresse / geoadresse /
katasterinformation must be set) run automatically via #[garde(custom(...))]
attributes on the generated types.
OpenAPI / JSON Schema
// schemars — JSON Schema (requires `schemars` feature)
let schema = schema_for!;
// utoipa — OpenAPI 3.1 (requires `utoipa` feature)
// All identifier types emit pattern, description, and example values:
// MaloId → { type: string, pattern: "^[0-9]{11}$", example: "51238696780" }
SQLx Integration
// Requires `sqlx` feature — implements Type, Encode, Decode for all identifiers
// Bind directly as typed identifier
query
.bind
.execute.await?;
// Decode directly — runs the same validation as new()
let id: MaloId = row.try_get?;
// Works in query_as! structs too
Documentation
- docs/architecture.md — Workspace layout, module tree, feature gate reference
- docs/generator.md — Internal code generator — running it, pipeline, inference rules
- docs/identifiers.md — All identifier types, validation rules, algorithms
- docs/validation.md — Cross-field business rules and
Validated<T> - docs/versioning.md — Schema versioning scheme and upgrade workflow
- docs/serialization.md — JSON format variants, extension-data map, round-trip guarantees
- docs/testing.md — Golden corpus, fuzz targets, proptest strategies
License
Licensed under either of Apache License 2.0 or MIT License, at your option.