1use crate::{SourceChange, SourceLinkChange};
2use dpm_model::WorkKind;
3use thiserror::Error;
4
5#[derive(Debug, Error)]
8pub enum InterchangeError {
9 #[error("MSPDI task at position {position}: invalid DPM metadata: {source}")]
11 Metadata {
12 position: usize,
14 #[source]
16 source: serde_json::Error,
17 },
18 #[error("MSPDI document is not well-formed XML: {source}")]
20 Xml {
21 #[source]
23 source: roxmltree::Error,
24 },
25 #[error(
27 "document root is {found}; expected <Project> in the http://schemas.microsoft.com/project namespace"
28 )]
29 NotMspdi {
30 found: String,
32 },
33 #[error("MSPDI project: {reason}")]
35 MalformedProject {
36 reason: String,
38 },
39 #[error("MSPDI task at position {position}: {reason}")]
41 MalformedTask {
42 position: usize,
44 reason: String,
46 },
47 #[error("MSPDI task UID {uid} cannot be positioned: {source}")]
49 OutlineOrder {
50 uid: i64,
52 #[source]
54 source: dpm_model::OrderError,
55 },
56 #[error("MSPDI task UID {uid} appears more than once")]
58 DuplicateUid {
59 uid: i64,
61 },
62 #[error(
64 "MSPDI tasks UID {first} and UID {second} resolve to the same work identity {identity}"
65 )]
66 DuplicateIdentity {
67 first: i64,
69 second: i64,
71 identity: String,
73 },
74 #[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 count: usize,
81 first_uid: i64,
83 },
84 #[error(
86 "matching existing work by title path is ambiguous, so nothing is matched: {}",
87 ambiguities.join("; ")
88 )]
89 AmbiguousMatch {
90 ambiguities: Vec<String>,
92 },
93 #[error("unknown project key {key}")]
95 UnknownProject {
96 key: String,
98 },
99 #[error("key {key} for MSPDI task UID {uid} is already used; choose another key prefix")]
101 KeyCollision {
102 key: String,
104 uid: i64,
106 },
107 #[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 uid: i64,
115 guid: Option<String>,
117 key: String,
119 from: WorkKind,
121 to: WorkKind,
123 },
124 #[error("{change}; refused: {source}")]
126 Refused {
127 change: SourceChange,
129 #[source]
131 source: Box<dyn std::error::Error + Send + Sync + 'static>,
132 },
133 #[error("{change}; refused: {source}")]
136 RefusedLink {
137 change: Box<SourceLinkChange>,
139 #[source]
141 source: Box<dyn std::error::Error + Send + Sync + 'static>,
142 },
143 #[error("time zone {time_zone:?} is not an IANA time zone name such as Europe/Berlin")]
145 InvalidTimeZone {
146 time_zone: String,
148 },
149 #[error(
151 "the workspace calendars use time zone {workspace}, not {requested}; import the document's calendars in {workspace}"
152 )]
153 TimeZoneMismatch {
154 workspace: String,
156 requested: String,
158 },
159 #[error("work {key} cannot be exported as MSPDI: {reason}")]
161 Unrepresentable {
162 key: String,
164 reason: String,
166 },
167}
168
169impl InterchangeError {
170 #[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}