edifact-mapper 0.8.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! Error types for the edifact-mapper facade crate.

/// Errors that can occur when using the [`Mapper`](crate::Mapper) API.
#[derive(Debug, thiserror::Error)]
pub enum MapperError {
    /// The specified data directory does not exist on disk.
    #[error("Data directory not found: {path}")]
    DataDirNotFound { path: String },

    /// No data bundle file found for the requested format version.
    #[error("No data bundle found for format version {fv}")]
    BundleNotFound { fv: String },

    /// The requested variant (e.g., "UTILMD_Strom") is not present in the bundle.
    #[error("No variant '{variant}' in bundle for {fv}")]
    VariantNotFound { fv: String, variant: String },

    /// No mapping engine definitions exist for the given PID within the variant.
    #[error("No mapping engine for PID {pid} in {fv}/{variant}")]
    PidNotFound {
        fv: String,
        variant: String,
        pid: String,
    },

    /// An error from the MIG assembly layer.
    #[error("Assembly error: {0}")]
    Assembly(#[from] mig_assembly::AssemblyError),

    /// An error from the TOML mapping engine.
    #[error("Mapping error: {0}")]
    Mapping(#[from] mig_bo4e::MappingError),

    /// JSON serialization failed (e.g., when converting a typed struct to JSON).
    #[error("Serialization error: {0}")]
    Serialization(String),

    /// The variant has no MIG schema (required for reverse pipeline).
    #[error("No MIG schema for {fv}/{variant}")]
    NoMigSchema { fv: String, variant: String },

    /// The BO4E input fills a group's segments but not the segment that opens
    /// the group in the MIG (e.g. `Zaehler.geraeteNummer` without
    /// `Zaehler.zaehlertypMerkmal` would yield SG10 `CAV` without `CCI`).
    /// Rendering it would produce EDIFACT no receiver can assemble, so the
    /// conversion is refused. Boxed to keep `MapperError` small.
    #[error(transparent)]
    MissingGroupEntrySegment(Box<GroupEntrySegmentError>),

    /// An [`EnvelopeOptions`](crate::EnvelopeOptions) date or time is not the
    /// shape `UNB` takes.
    ///
    /// The value is written into the interchange header verbatim, so a wrongly
    /// formatted one produces a malformed outermost envelope — the segment
    /// whose defects surface at the receiving gateway rather than anywhere the
    /// sender can see them. `datum` is `yymmdd` and `zeit` is `hhmm`, both
    /// digits only.
    #[error("UNB {field} must be {expected} ({digits} digits), got {value:?}")]
    MalformedEnvelopeDateTime {
        /// `"datum"` or `"zeit"`.
        field: &'static str,
        /// The expected pattern, e.g. `"yymmdd"`.
        expected: &'static str,
        /// How many digits that pattern is.
        digits: usize,
        /// What the caller supplied.
        value: String,
    },

    /// The data bundle was produced by a different release than this crate.
    ///
    /// The bundle's serialisation format can be current while its mappings,
    /// schemas and code lists are from another era — that combination loads
    /// cleanly and renders a smaller message rather than failing (issue #158).
    #[error(
        "Data bundle for {fv} was produced by {}, but this is edifact-mapper {expected}. \
         Re-download the bundle for this release (`edifact-data update`), or call \
         DataDir::allow_bundle_from_other_release(true) if the mismatch is deliberate. \
         Bundle: {path}",
        built_by.as_deref().unwrap_or("a release before bundles recorded one")
    )]
    BundleFromOtherRelease {
        /// Format version of the bundle.
        fv: String,
        /// The release that produced it, if it recorded one.
        built_by: Option<String>,
        /// The release reading it.
        expected: String,
        /// Where the bundle was read from.
        path: String,
    },

    /// A standard I/O error.
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

/// Details of [`MapperError::MissingGroupEntrySegment`].
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
    "Cannot render PID {pid}: group {group_path} ({source_path}) would contain {present_segments:?} but not its entry segment '{entry_segment}'{}",
    entry_segment_hint(.entities, .entry_fields)
)]
pub struct GroupEntrySegmentError {
    pub pid: String,
    /// MIG group path, e.g. `"SG4.SG8.SG10"`.
    pub group_path: String,
    /// The group in TOML `source_path` notation, e.g. `"sg4.sg8_z03.sg10"`.
    pub source_path: String,
    /// Tag of the missing entry segment, e.g. `"CCI"`.
    pub entry_segment: String,
    /// Tags of the segments the group would have carried.
    pub present_segments: Vec<String>,
    /// BO4E entities mapped from this group (e.g. `["Zaehler"]`).
    pub entities: Vec<String>,
    /// `Entity.field` values the entry segment is built from
    /// (e.g. `["Zaehler.zaehlertypMerkmal"]`); supplying one fixes the input.
    pub entry_fields: Vec<String>,
}

fn entry_segment_hint(entities: &[String], entry_fields: &[String]) -> String {
    if !entry_fields.is_empty() {
        format!(" (set {})", entry_fields.join(" or "))
    } else if !entities.is_empty() {
        format!(" (entity {})", entities.join(", "))
    } else {
        String::new()
    }
}