Skip to main content

edifact_mapper/
error.rs

1//! Error types for the edifact-mapper facade crate.
2
3/// Errors that can occur when using the [`Mapper`](crate::Mapper) API.
4#[derive(Debug, thiserror::Error)]
5pub enum MapperError {
6    /// The specified data directory does not exist on disk.
7    #[error("Data directory not found: {path}")]
8    DataDirNotFound { path: String },
9
10    /// No data bundle file found for the requested format version.
11    #[error("No data bundle found for format version {fv}")]
12    BundleNotFound { fv: String },
13
14    /// The requested variant (e.g., "UTILMD_Strom") is not present in the bundle.
15    #[error("No variant '{variant}' in bundle for {fv}")]
16    VariantNotFound { fv: String, variant: String },
17
18    /// No mapping engine definitions exist for the given PID within the variant.
19    #[error("No mapping engine for PID {pid} in {fv}/{variant}")]
20    PidNotFound {
21        fv: String,
22        variant: String,
23        pid: String,
24    },
25
26    /// An error from the MIG assembly layer.
27    #[error("Assembly error: {0}")]
28    Assembly(#[from] mig_assembly::AssemblyError),
29
30    /// An error from the TOML mapping engine.
31    #[error("Mapping error: {0}")]
32    Mapping(#[from] mig_bo4e::MappingError),
33
34    /// JSON serialization failed (e.g., when converting a typed struct to JSON).
35    #[error("Serialization error: {0}")]
36    Serialization(String),
37
38    /// The variant has no MIG schema (required for reverse pipeline).
39    #[error("No MIG schema for {fv}/{variant}")]
40    NoMigSchema { fv: String, variant: String },
41
42    /// The BO4E input fills a group's segments but not the segment that opens
43    /// the group in the MIG (e.g. `Zaehler.geraeteNummer` without
44    /// `Zaehler.zaehlertypMerkmal` would yield SG10 `CAV` without `CCI`).
45    /// Rendering it would produce EDIFACT no receiver can assemble, so the
46    /// conversion is refused. Boxed to keep `MapperError` small.
47    #[error(transparent)]
48    MissingGroupEntrySegment(Box<GroupEntrySegmentError>),
49
50    /// An [`EnvelopeOptions`](crate::EnvelopeOptions) date or time is not the
51    /// shape `UNB` takes.
52    ///
53    /// The value is written into the interchange header verbatim, so a wrongly
54    /// formatted one produces a malformed outermost envelope — the segment
55    /// whose defects surface at the receiving gateway rather than anywhere the
56    /// sender can see them. `datum` is `yymmdd` and `zeit` is `hhmm`, both
57    /// digits only.
58    #[error("UNB {field} must be {expected} ({digits} digits), got {value:?}")]
59    MalformedEnvelopeDateTime {
60        /// `"datum"` or `"zeit"`.
61        field: &'static str,
62        /// The expected pattern, e.g. `"yymmdd"`.
63        expected: &'static str,
64        /// How many digits that pattern is.
65        digits: usize,
66        /// What the caller supplied.
67        value: String,
68    },
69
70    /// The data bundle was produced by a different release than this crate.
71    ///
72    /// The bundle's serialisation format can be current while its mappings,
73    /// schemas and code lists are from another era — that combination loads
74    /// cleanly and renders a smaller message rather than failing (issue #158).
75    #[error(
76        "Data bundle for {fv} was produced by {}, but this is edifact-mapper {expected}. \
77         Re-download the bundle for this release (`edifact-data update`), or call \
78         DataDir::allow_bundle_from_other_release(true) if the mismatch is deliberate. \
79         Bundle: {path}",
80        built_by.as_deref().unwrap_or("a release before bundles recorded one")
81    )]
82    BundleFromOtherRelease {
83        /// Format version of the bundle.
84        fv: String,
85        /// The release that produced it, if it recorded one.
86        built_by: Option<String>,
87        /// The release reading it.
88        expected: String,
89        /// Where the bundle was read from.
90        path: String,
91    },
92
93    /// A standard I/O error.
94    #[error("IO error: {0}")]
95    Io(#[from] std::io::Error),
96}
97
98/// Details of [`MapperError::MissingGroupEntrySegment`].
99#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
100#[error(
101    "Cannot render PID {pid}: group {group_path} ({source_path}) would contain {present_segments:?} but not its entry segment '{entry_segment}'{}",
102    entry_segment_hint(.entities, .entry_fields)
103)]
104pub struct GroupEntrySegmentError {
105    pub pid: String,
106    /// MIG group path, e.g. `"SG4.SG8.SG10"`.
107    pub group_path: String,
108    /// The group in TOML `source_path` notation, e.g. `"sg4.sg8_z03.sg10"`.
109    pub source_path: String,
110    /// Tag of the missing entry segment, e.g. `"CCI"`.
111    pub entry_segment: String,
112    /// Tags of the segments the group would have carried.
113    pub present_segments: Vec<String>,
114    /// BO4E entities mapped from this group (e.g. `["Zaehler"]`).
115    pub entities: Vec<String>,
116    /// `Entity.field` values the entry segment is built from
117    /// (e.g. `["Zaehler.zaehlertypMerkmal"]`); supplying one fixes the input.
118    pub entry_fields: Vec<String>,
119}
120
121fn entry_segment_hint(entities: &[String], entry_fields: &[String]) -> String {
122    if !entry_fields.is_empty() {
123        format!(" (set {})", entry_fields.join(" or "))
124    } else if !entities.is_empty() {
125        format!(" (entity {})", entities.join(", "))
126    } else {
127        String::new()
128    }
129}