Skip to main content

nextcloud_ocs/envelope/
meta.rs

1use serde::{Deserialize, Serialize};
2
3use super::OcsStatus;
4use super::text::lossy_string;
5
6/// The `meta` block of an OCS envelope: the verdict plus pagination hints.
7///
8/// Every field is held as a string because that is what Nextcloud documents
9/// (`statuscode`, `totalitems` and `itemsperpage` are declared as strings) —
10/// yet the server sends bare numbers for several of them, and an empty string
11/// where a count does not apply. Deserialisation accepts both forms and
12/// normalises to text; [`Meta::code`] parses the numeric verdict on demand.
13#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
14pub struct Meta {
15    #[serde(default, deserialize_with = "lossy_string")]
16    status: String,
17    #[serde(default, deserialize_with = "lossy_string")]
18    statuscode: String,
19    #[serde(default, deserialize_with = "lossy_string")]
20    message: String,
21    #[serde(default, deserialize_with = "lossy_string")]
22    totalitems: String,
23    #[serde(default, deserialize_with = "lossy_string")]
24    itemsperpage: String,
25}
26
27impl Meta {
28    /// Verdict text as sent, `"ok"` or `"failure"`.
29    pub fn status(&self) -> &str {
30        &self.status
31    }
32
33    /// `statuscode` as sent, usually `"100"`, `"200"` or a failure code.
34    pub fn statuscode(&self) -> &str {
35        &self.statuscode
36    }
37
38    /// Human-readable message, often carrying the failure reason.
39    pub fn message(&self) -> &str {
40        &self.message
41    }
42
43    /// Total number of items across all pages, when the endpoint paginates.
44    pub fn totalitems(&self) -> &str {
45        &self.totalitems
46    }
47
48    /// Items per page, when the endpoint paginates.
49    pub fn itemsperpage(&self) -> &str {
50        &self.itemsperpage
51    }
52
53    /// `statuscode` parsed as a number, `None` when absent or not a number.
54    pub fn code(&self) -> Option<u16> {
55        self.statuscode.trim().parse().ok()
56    }
57
58    /// Verdict from `statuscode`, falling back to the `status` text for
59    /// envelopes that omit the numeric code.
60    pub fn verdict(&self) -> Option<OcsStatus> {
61        self.code()
62            .map(OcsStatus::from_statuscode)
63            .or_else(|| OcsStatus::from_status_text(&self.status))
64    }
65
66    /// Whether the envelope reports success.
67    ///
68    /// An envelope that carries neither a readable code nor a readable status
69    /// is not a success: the absence of a verdict must not read as one.
70    pub fn is_ok(&self) -> bool {
71        self.verdict().is_some_and(OcsStatus::is_ok)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::Meta;
78    use crate::envelope::OcsStatus;
79
80    #[test]
81    fn accepts_string_fields() {
82        let meta: Meta = serde_json::from_str(
83            r#"{"status":"ok","statuscode":200,"message":"","totalitems":"","itemsperpage":""}"#,
84        )
85        .unwrap();
86        assert_eq!(meta.code(), Some(200));
87        assert!(meta.is_ok());
88    }
89
90    #[test]
91    fn accepts_numeric_fields() {
92        let meta: Meta =
93            serde_json::from_str(r#"{"status":"ok","statuscode":200,"totalitems":3}"#).unwrap();
94        assert_eq!(meta.statuscode(), "200");
95        assert_eq!(meta.totalitems(), "3");
96        assert_eq!(meta.verdict(), Some(OcsStatus::Ok));
97    }
98
99    #[test]
100    fn missing_fields_default_to_empty() {
101        let meta: Meta = serde_json::from_str(r#"{"status":"failure"}"#).unwrap();
102        assert_eq!(meta.statuscode(), "");
103        assert_eq!(meta.code(), None);
104        assert_eq!(meta.verdict(), Some(OcsStatus::Failure));
105        assert!(!meta.is_ok());
106    }
107
108    #[test]
109    fn unknown_verdict_is_not_success() {
110        let meta: Meta = serde_json::from_str(r#"{"status":"","statuscode":""}"#).unwrap();
111        assert_eq!(meta.verdict(), None);
112        assert!(!meta.is_ok());
113    }
114}