pageseeder_api/
model.rs

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// Result
10
11#[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// Services
30
31#[derive(Debug, Clone)]
32pub enum Service<'a> {
33    GetGroup {
34        /// Group to get.
35        group: &'a str,
36    },
37    GetUri {
38        /// Member to get details as.
39        member: &'a str,
40        /// URI to get.
41        uri: &'a str,
42    },
43    GetUriHistory {
44        /// Group URI is in.
45        group: &'a str,
46        /// URI to get history for.
47        uri: &'a str,
48    },
49    GetUrisHistory {
50        /// Group URIs are in.
51        group: &'a str,
52    },
53    GetUriFragment {
54        /// Member to get fragment as.
55        member: &'a str,
56        /// Group URI is in.
57        group: &'a str,
58        /// URI of document to get fragment from.
59        uri: &'a str,
60        /// ID of the fragment to return.
61        fragment: &'a str,
62    },
63    UriExport {
64        /// Member to export as.
65        member: &'a str,
66        /// URI to export.
67        uri: &'a str,
68    },
69    GroupSearch {
70        /// Group to search.
71        group: &'a str,
72    },
73    ThreadProgress {
74        /// Thread ID to get progress for.
75        id: &'a str,
76    },
77    PutUriFragment {
78        /// Member to put fragment as.
79        member: &'a str,
80        /// Group URI is in.
81        group: &'a str,
82        /// URI of document.
83        uri: &'a str,
84        /// ID of fragment to put.
85        fragment: &'a str,
86    },
87    AddUriFragment {
88        /// Member to add fragment as.
89        member: &'a str,
90        /// Group URI is in.
91        group: &'a str,
92        /// URI of document.
93        uri: &'a str,
94    },
95    Upload,
96    ClearLoadingZone {
97        /// Member owning the loading zone.
98        member: &'a str,
99        /// Group the loading zone is in.
100        group: &'a str,
101    },
102    UnzipLoadingZone {
103        /// Member owning the loading zone.
104        member: &'a str,
105        /// Group the loading zone is in.
106        group: &'a str,
107    },
108    StartLoading {
109        /// Member owning the loading zone.
110        member: &'a str,
111        /// Group the loading zone is in.
112        group: &'a str,
113    },
114    DownloadMemberResource {
115        /// Group the resource was exported from.
116        group: &'a str,
117        /// File name of the resource.
118        filename: &'a str,
119    },
120    CreateUriVersion {
121        /// Member to create version as.
122        member: &'a str,
123        /// Group URI is in.
124        group: &'a str,
125        /// URI of document.
126        uri: &'a str,
127    },
128}
129
130impl Service<'_> {
131    /// Returns the url path for this service.
132    /// e.g. GetGroup => /ps/service/groups/{group}
133    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// Elements
185
186#[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// TODO implement if not PS member + other event children
299// see: https://dev.pageseeder.com/api/element_reference/element_author.html
300#[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// Export
371
372#[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    /// Returns true if thread is still running.
386    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// Search
457
458#[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// Uploading
496
497#[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/// Should have exactly one of version or thread.
556#[derive(Debug, Deserialize)]
557#[serde(rename = "version-creation")]
558pub struct VersionCreation {
559    pub version: Option<Version>,
560    pub thread: Option<Thread>,
561}