1use std::fmt::Display;
2
3use chrono::{DateTime, Utc};
4use serde::Deserialize;
5
6use psml::model::{Description, Fragments, Labels, Locator};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
12pub enum PSError {
13 #[error("failed while communicating with server: {0}")]
14 CommunicationError(#[from] reqwest::Error),
15 #[error("failed while parsing server response: {0}")]
16 ParseError(#[from] quick_xml::DeError),
17 #[error("failed while authenticating with the token: {msg}")]
18 TokenError { msg: String },
19 #[error("there was an issue with an API request with ID {id} ({req}): {msg}")]
20 ApiError {
21 id: String,
22 req: String,
23 msg: String,
24 },
25}
26
27pub type PSResult<T> = Result<T, PSError>;
28
29#[derive(Debug, Clone)]
32pub enum Service<'a> {
33 GetGroup {
34 group: &'a str,
36 },
37 GetUri {
38 member: &'a str,
40 uri: &'a str,
42 },
43 GetUriHistory {
44 group: &'a str,
46 uri: &'a str,
48 },
49 GetUrisHistory {
50 group: &'a str,
52 },
53 GetUriFragment {
54 member: &'a str,
56 group: &'a str,
58 uri: &'a str,
60 fragment: &'a str,
62 },
63 UriExport {
64 member: &'a str,
66 uri: &'a str,
68 },
69 GroupSearch {
70 group: &'a str,
72 },
73 ThreadProgress {
74 id: &'a str,
76 },
77 PutUriFragment {
78 member: &'a str,
80 group: &'a str,
82 uri: &'a str,
84 fragment: &'a str,
86 },
87 AddUriFragment {
88 member: &'a str,
90 group: &'a str,
92 uri: &'a str,
94 },
95 Upload,
96 ClearLoadingZone {
97 member: &'a str,
99 group: &'a str,
101 },
102 UnzipLoadingZone {
103 member: &'a str,
105 group: &'a str,
107 },
108 StartLoading {
109 member: &'a str,
111 group: &'a str,
113 },
114 DownloadMemberResource {
115 group: &'a str,
117 filename: &'a str,
119 },
120 CreateUriVersion {
121 member: &'a str,
123 group: &'a str,
125 uri: &'a str,
127 },
128}
129
130impl Service<'_> {
131 pub fn url_path(&self) -> String {
134 let path = match self {
135 Self::GetGroup { group } => format!("groups/{group}"),
136 Self::GetUri { member, uri } => format!("members/{member}/uris/{uri}"),
137 Self::GetUriHistory { group, uri } => format!("groups/{group}/uris/{uri}/history"),
138 Self::GetUrisHistory { group } => format!("groups/{group}/uris/history"),
139 Self::GetUriFragment {
140 member,
141 group,
142 uri,
143 fragment,
144 } => format!("members/{member}/groups/{group}/uris/{uri}/fragments/{fragment}"),
145 Self::UriExport { member, uri } => format!("members/{member}/uris/{uri}/export"),
146 Self::GroupSearch { group } => format!("groups/{group}/search"),
147 Self::ThreadProgress { id } => format!("threads/{id}/progress"),
148 Self::PutUriFragment {
149 member,
150 group,
151 uri,
152 fragment,
153 } => format!("members/{member}/groups/{group}/uris/{uri}/fragments/{fragment}"),
154 Self::AddUriFragment { member, group, uri } => {
155 format!("members/{member}/groups/{group}/uris/{uri}/fragments")
156 }
157 Self::Upload => return "/ps/servlet/upload".to_string(),
158 Self::ClearLoadingZone { member, group } => {
159 format!("members/{member}/groups/{group}/loadingzone/clear")
160 }
161 Self::UnzipLoadingZone { member, group } => {
162 format!("members/{member}/groups/{group}/loadingzone/unzip")
163 }
164 Self::StartLoading { member, group } => {
165 format!("members/{member}/groups/{group}/loadingzone/start")
166 }
167 Self::DownloadMemberResource { group, filename } => {
168 return format!("/ps/member-resource/{group}/{filename}")
169 }
170 Self::CreateUriVersion { member, group, uri } => {
171 format!("members/{member}/groups/{group}/uris/{uri}/versions")
172 }
173 };
174 format!("/ps/service/{path}")
175 }
176}
177
178impl From<Service<'_>> for String {
179 fn from(val: Service<'_>) -> Self {
180 val.url_path()
181 }
182}
183
184#[derive(Debug, Deserialize)]
187pub struct Error {
188 #[serde(rename = "@id")]
189 pub id: String,
190 pub request: String,
191 pub message: String,
192}
193
194#[derive(Debug, Deserialize)]
195#[serde(rename_all = "lowercase")]
196pub enum PSGroupAccess {
197 Public,
198 Member,
199}
200
201#[derive(Debug, Deserialize)]
202pub struct Group {
203 #[serde(rename = "@id")]
204 pub id: u32,
205 #[serde(rename = "@name")]
206 pub name: String,
207 #[serde(rename = "@owner")]
208 pub owner: String,
209 #[serde(rename = "@description")]
210 pub description: String,
211 #[serde(rename = "@access")]
212 pub access: PSGroupAccess,
213}
214
215impl Group {
216 pub fn short_name(&self) -> &str {
217 self.name
218 .rsplit_once('-')
219 .unwrap_or_else(|| panic!("Group name has no '-': {}", self.name))
220 .1
221 }
222}
223
224#[derive(Debug, Deserialize)]
225pub struct Uri {
226 #[serde(rename = "@id")]
227 pub id: String,
228 #[serde(rename = "@scheme")]
229 pub scheme: String,
230 #[serde(rename = "@host")]
231 pub host: String,
232 #[serde(rename = "@port")]
233 pub port: String,
234 #[serde(rename = "@path")]
235 pub path: String,
236 #[serde(rename = "@decodedpath")]
237 pub decodedpath: String,
238 #[serde(rename = "@external")]
239 pub external: bool,
240 #[serde(rename = "@archived")]
241 pub archived: Option<bool>,
242 #[serde(rename = "@folder")]
243 pub folder: Option<bool>,
244 #[serde(rename = "@docid")]
245 pub docid: Option<String>,
246 #[serde(rename = "@mediatype")]
247 pub mediatype: Option<String>,
248 #[serde(rename = "@documenttype")]
249 pub documenttype: Option<String>,
250 #[serde(rename = "@title")]
251 pub title: Option<String>,
252 #[serde(rename = "@created")]
253 pub created: Option<DateTime<Utc>>,
254 #[serde(rename = "@modified")]
255 pub modified: Option<DateTime<Utc>>,
256}
257
258#[derive(Debug, Deserialize)]
259#[serde(rename_all = "lowercase")]
260pub enum EventType {
261 Upload,
262 Creation,
263 Move,
264 Modification,
265 Structure,
266 Workflow,
267 Version,
268 Edit,
269 Draft,
270 Note,
271 Xref,
272 Image,
273 Comment,
274 Task,
275}
276
277impl From<EventType> for String {
278 fn from(val: EventType) -> Self {
279 match val {
280 EventType::Upload => "upload ".to_string(),
281 EventType::Creation => "creation ".to_string(),
282 EventType::Move => "move ".to_string(),
283 EventType::Modification => "modification ".to_string(),
284 EventType::Structure => "structure ".to_string(),
285 EventType::Workflow => "workflow ".to_string(),
286 EventType::Version => "version ".to_string(),
287 EventType::Edit => "edit ".to_string(),
288 EventType::Draft => "draft ".to_string(),
289 EventType::Note => "note ".to_string(),
290 EventType::Xref => "xref ".to_string(),
291 EventType::Image => "image ".to_string(),
292 EventType::Comment => "comment ".to_string(),
293 EventType::Task => "task ".to_string(),
294 }
295 }
296}
297
298#[derive(Debug, Deserialize)]
301pub struct Author {
302 #[serde(rename = "@id")]
303 pub id: String,
304 #[serde(rename = "@firstname")]
305 pub firstname: String,
306 #[serde(rename = "@surname")]
307 pub surname: String,
308 #[serde(rename = "@username")]
309 pub username: Option<String>,
310 #[serde(rename = "@status")]
311 pub status: Option<String>,
312}
313
314#[derive(Debug, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum EventContent {
317 Author(Author),
318 Labels(()),
319 Change(()),
320 Uri(Uri),
321}
322
323#[derive(Debug, Deserialize)]
324pub struct Event {
325 #[serde(rename = "@id")]
326 pub id: String,
327 #[serde(rename = "@datetime")]
328 pub datetime: Option<DateTime<Utc>>,
329 #[serde(rename = "@type")]
330 pub event_type: EventType,
331 #[serde(rename = "@fragment")]
332 pub fragment: Option<String>,
333 #[serde(rename = "@title")]
334 pub title: Option<String>,
335 #[serde(rename = "@uriid")]
336 pub uriid: Option<String>,
337 #[serde(rename = "@targetfragment")]
338 pub targetfragment: Option<String>,
339 #[serde(rename = "@version")]
340 pub version: Option<String>,
341 #[serde(rename = "$value", default)]
342 pub content: Vec<EventContent>,
343}
344
345#[derive(Debug, Deserialize)]
346pub struct UriHistory {
347 #[serde(rename = "@events")]
348 pub event_types: String,
349 #[serde(rename = "$value")]
350 pub events: Vec<Event>,
351}
352
353#[derive(Debug, Deserialize)]
354#[serde(rename = "document-fragment")]
355pub struct DocumentFragment {
356 pub locator: Option<Locator>,
357 #[serde(rename = "$value")]
358 pub fragment: Option<Fragments>,
359}
360
361#[derive(Debug, Deserialize)]
362#[serde(rename = "fragment-creation")]
363pub struct FragmentCreation {
364 #[serde(rename = "@unresolved-xrefs")]
365 pub unresolved_xrefs: Option<bool>,
366 #[serde(rename = "document-fragment")]
367 pub document_fragment: DocumentFragment,
368}
369
370#[derive(Debug, Deserialize)]
373#[serde(rename_all = "lowercase")]
374pub enum ThreadStatus {
375 Initialised,
376 InProgress,
377 Error,
378 Warning,
379 Cancelled,
380 Failed,
381 Completed,
382}
383
384impl ThreadStatus {
385 pub fn running(&self) -> bool {
387 matches!(self, Self::Initialised | Self::InProgress)
388 }
389}
390
391impl Display for ThreadStatus {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 match self {
394 Self::Initialised => write!(f, "initialised"),
395 Self::InProgress => write!(f, "inprogress"),
396 Self::Error => write!(f, "error"),
397 Self::Warning => write!(f, "warning"),
398 Self::Cancelled => write!(f, "cancelled"),
399 Self::Failed => write!(f, "failed"),
400 Self::Completed => write!(f, "completed"),
401 }
402 }
403}
404
405#[derive(Debug, Deserialize)]
406#[serde(rename = "zip")]
407pub struct ThreadZip {
408 #[serde(rename = "$text")]
409 pub filename: String,
410}
411
412#[derive(Debug, Deserialize)]
413#[serde(rename = "message")]
414pub struct ThreadMessage {
415 #[serde(rename = "$text")]
416 pub message: String,
417}
418
419#[derive(Debug, Deserialize)]
420#[serde(rename = "processing")]
421pub struct ThreadProcessing {
422 #[serde(rename = "@current")]
423 pub current: u64,
424 #[serde(rename = "@total")]
425 pub total: u64,
426}
427
428#[derive(Debug, Deserialize)]
429#[serde(rename = "packaging")]
430pub struct ThreadPackaging {
431 #[serde(rename = "@current")]
432 pub current: u64,
433 #[serde(rename = "@total")]
434 pub total: u64,
435}
436
437#[derive(Debug, Deserialize)]
438#[serde(rename = "thread")]
439pub struct Thread {
440 #[serde(rename = "@id")]
441 pub id: String,
442 #[serde(rename = "@name")]
443 pub name: String,
444 #[serde(rename = "@username")]
445 pub username: String,
446 #[serde(rename = "@groupid")]
447 pub groupid: String,
448 #[serde(rename = "@status")]
449 pub status: ThreadStatus,
450 pub processing: Option<ThreadProcessing>,
451 pub packaging: Option<ThreadPackaging>,
452 pub zip: Option<ThreadZip>,
453 pub message: Option<ThreadMessage>,
454}
455
456#[derive(Debug, Deserialize)]
459pub struct SearchResultField {
460 #[serde(rename = "@name")]
461 pub name: String,
462 #[serde(rename = "$text", default)]
463 pub value: String,
464}
465
466#[derive(Debug, Deserialize)]
467pub struct SearchResult {
468 #[serde(rename = "field", default)]
469 pub fields: Vec<SearchResultField>,
470}
471
472#[derive(Debug, Deserialize)]
473pub struct SearchResultPage {
474 #[serde(rename = "@page")]
475 pub page: u64,
476 #[serde(rename = "@page-size")]
477 pub page_size: u64,
478 #[serde(rename = "@total-pages")]
479 pub total_pages: u64,
480 #[serde(rename = "@total-results")]
481 pub total_results: u64,
482 #[serde(rename = "@first-result")]
483 pub first_result: Option<u64>,
484 #[serde(rename = "@last-result")]
485 pub last_result: Option<u64>,
486 #[serde(rename = "result", default)]
487 pub results: Vec<SearchResult>,
488}
489
490#[derive(Debug, Deserialize)]
491pub struct SearchResponse {
492 pub results: SearchResultPage,
493}
494
495#[derive(Debug, Deserialize)]
498pub struct File {
499 #[serde(rename = "@name")]
500 pub name: String,
501 #[serde(rename = "@path")]
502 pub path: String,
503 #[serde(rename = "@type")]
504 pub ftype: String,
505}
506
507#[derive(Debug, Deserialize)]
508pub struct Upload {
509 #[serde(rename = "@member")]
510 pub member: String,
511 #[serde(rename = "@uploadid")]
512 pub uploadid: Option<String>,
513 #[serde(rename = "@status")]
514 pub status: Option<String>,
515 #[serde(rename = "@max-workflow-notifications")]
516 pub max_workflow_notifications: u8,
517 pub message: Option<String>,
518 pub uri: Option<Uri>,
519 pub file: Option<File>,
520}
521
522#[derive(Debug, Deserialize)]
523#[serde(rename = "load-clear")]
524pub struct LoadClear {
525 #[serde(rename = "@filesremoved")]
526 pub files_removed: usize,
527}
528
529#[derive(Debug, Deserialize)]
530#[serde(rename = "load-unzip")]
531pub struct LoadUnzip {
532 pub thread: Thread,
533}
534
535#[derive(Debug, Deserialize)]
536#[serde(rename = "load-start")]
537pub struct LoadStart {
538 pub thread: Thread,
539}
540
541#[derive(Debug, Deserialize)]
542#[serde(rename = "version")]
543pub struct Version {
544 #[serde(rename = "@id")]
545 pub id: String,
546 #[serde(rename = "@name")]
547 pub name: String,
548 #[serde(rename = "@created")]
549 pub created: String,
550 pub author: Author,
551 pub description: Option<Description>,
552 pub labels: Labels,
553}
554
555#[derive(Debug, Deserialize)]
557#[serde(rename = "version-creation")]
558pub struct VersionCreation {
559 pub version: Option<Version>,
560 pub thread: Option<Thread>,
561}