Skip to main content

dpm_interchange/
error.rs

1use crate::{SourceChange, SourceLinkChange};
2use dpm_model::WorkKind;
3use thiserror::Error;
4
5/// Interchange failure; every variant leaves the workspace untouched because imports only build
6/// a candidate for review.
7#[derive(Debug, Error)]
8pub enum InterchangeError {
9    /// A namespaced DPM metadata value is malformed.
10    #[error("MSPDI task at position {position}: invalid DPM metadata: {source}")]
11    Metadata {
12        /// One-based task position.
13        position: usize,
14        /// Original JSON diagnostic.
15        #[source]
16        source: serde_json::Error,
17    },
18    /// The document is not well-formed XML.
19    #[error("MSPDI document is not well-formed XML: {source}")]
20    Xml {
21        /// Parser diagnostic with position.
22        #[source]
23        source: roxmltree::Error,
24    },
25    /// The root element is not a Microsoft Project XML project.
26    #[error(
27        "document root is {found}; expected <Project> in the http://schemas.microsoft.com/project namespace"
28    )]
29    NotMspdi {
30        /// Qualified name of the root element found.
31        found: String,
32    },
33    /// A project-level element violates the MSPDI structure DPM relies on.
34    #[error("MSPDI project: {reason}")]
35    MalformedProject {
36        /// Violated structural rule.
37        reason: String,
38    },
39    /// A task or link element violates the MSPDI structure DPM relies on.
40    #[error("MSPDI task at position {position}: {reason}")]
41    MalformedTask {
42        /// One-based position of the task element in the document.
43        position: usize,
44        /// Violated structural rule.
45        reason: String,
46    },
47    /// The imported outline cannot be assigned a valid sibling position.
48    #[error("MSPDI task UID {uid} cannot be positioned: {source}")]
49    OutlineOrder {
50        /// Source task UID.
51        uid: i64,
52        /// Position allocation failure.
53        #[source]
54        source: dpm_model::OrderError,
55    },
56    /// Two task elements share a unique identifier.
57    #[error("MSPDI task UID {uid} appears more than once")]
58    DuplicateUid {
59        /// Repeated source UID.
60        uid: i64,
61    },
62    /// Two task elements resolve to the same local work identity.
63    #[error(
64        "MSPDI tasks UID {first} and UID {second} resolve to the same work identity {identity}"
65    )]
66    DuplicateIdentity {
67        /// First task UID.
68        first: i64,
69        /// Second task UID.
70        second: i64,
71        /// Shared identity.
72        identity: String,
73    },
74    /// Neither the document nor some of its tasks carry a GUID, and the import names no source.
75    #[error(
76        "MSPDI document has no project GUID and {count} task(s) without a GUID (first UID {first_uid}); pass an explicit key prefix that names this source: work identity is then derived from the target project, the key prefix and the task UID, so re-importing with the same prefix updates the same work and another prefix plans separate work"
77    )]
78    SourceScopeRequired {
79        /// Imported tasks without a GUID.
80        count: usize,
81        /// First such task UID in document order.
82        first_uid: i64,
83    },
84    /// Matching GUID-less tasks to existing work by title path is not unique.
85    #[error(
86        "matching existing work by title path is ambiguous, so nothing is matched: {}",
87        ambiguities.join("; ")
88    )]
89    AmbiguousMatch {
90        /// Every ambiguous task or path, with the local work keys involved.
91        ambiguities: Vec<String>,
92    },
93    /// The requested project key does not exist in the workspace.
94    #[error("unknown project key {key}")]
95    UnknownProject {
96        /// Requested project key.
97        key: String,
98    },
99    /// A generated key is already used by other work.
100    #[error("key {key} for MSPDI task UID {uid} is already used; choose another key prefix")]
101    KeyCollision {
102        /// Generated work key.
103        key: String,
104        /// Source task UID.
105        uid: i64,
106    },
107    /// A source task would change the kind of existing work, which reviewed plan changes forbid.
108    #[error(
109        "MSPDI task UID {uid} (GUID {}) would change {key} from {from:?} to {to:?}; the kind of existing work is fixed, so restore the source outline or milestone flag, or plan new work",
110        guid.as_deref().unwrap_or("none")
111    )]
112    KindChange {
113        /// Source task UID.
114        uid: i64,
115        /// Source task GUID, when present.
116        guid: Option<String>,
117        /// Existing work key.
118        key: String,
119        /// Local kind.
120        from: WorkKind,
121        /// Kind the source implies.
122        to: WorkKind,
123    },
124    /// Review refused the candidate at work that a source task maps to.
125    #[error("{change}; refused: {source}")]
126    Refused {
127        /// Source task and the changes it attempts.
128        change: SourceChange,
129        /// Review refusal naming only local work.
130        #[source]
131        source: Box<dyn std::error::Error + Send + Sync + 'static>,
132    },
133    /// Review refused the candidate at a dependency that a source link maps to, or that the
134    /// candidate removes because the document omits the link.
135    #[error("{change}; refused: {source}")]
136    RefusedLink {
137        /// Source link and the change it attempts.
138        change: Box<SourceLinkChange>,
139        /// Review refusal naming only the dependency identity.
140        #[source]
141        source: Box<dyn std::error::Error + Send + Sync + 'static>,
142    },
143    /// The time zone given for the document's calendars is not an IANA time zone name.
144    #[error("time zone {time_zone:?} is not an IANA time zone name such as Europe/Berlin")]
145    InvalidTimeZone {
146        /// Requested time zone.
147        time_zone: String,
148    },
149    /// The workspace already reads its calendars in another time zone.
150    #[error(
151        "the workspace calendars use time zone {workspace}, not {requested}; import the document's calendars in {workspace}"
152    )]
153    TimeZoneMismatch {
154        /// Time zone of the workspace calendars.
155        workspace: String,
156        /// Time zone given for the document.
157        requested: String,
158    },
159    /// A value cannot be written as MSPDI.
160    #[error("work {key} cannot be exported as MSPDI: {reason}")]
161    Unrepresentable {
162        /// Work key.
163        key: String,
164        /// Reason the value has no MSPDI form.
165        reason: String,
166    },
167}
168
169impl InterchangeError {
170    /// Stable machine category shared by the CLI and agent adapters.
171    #[must_use]
172    pub fn code(&self) -> &'static str {
173        match self {
174            Self::UnknownProject { .. } => "not_found",
175            Self::KeyCollision { .. }
176            | Self::Unrepresentable { .. }
177            | Self::OutlineOrder { .. }
178            | Self::KindChange { .. }
179            | Self::AmbiguousMatch { .. }
180            | Self::Refused { .. }
181            | Self::RefusedLink { .. } => "invalid_command",
182            Self::Metadata { .. }
183            | Self::Xml { .. }
184            | Self::NotMspdi { .. }
185            | Self::MalformedProject { .. }
186            | Self::MalformedTask { .. }
187            | Self::DuplicateUid { .. }
188            | Self::DuplicateIdentity { .. }
189            | Self::SourceScopeRequired { .. }
190            | Self::InvalidTimeZone { .. }
191            | Self::TimeZoneMismatch { .. } => "invalid_request",
192        }
193    }
194}