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