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