Skip to main content

writ_client/
page.rs

1//! Uniform list envelope (DESIGN.md §6).
2//!
3//! The daemon is inconsistent on the wire: some list endpoints answer
4//! `{"data": [...], "count": n}`, runs answer `{"data": [...], "count": n, "total": n}`,
5//! and monitors/automations/selectors answer a bare JSON array. Every list method in
6//! this SDK normalizes all three into the same [`Page`] so callers never see the
7//! difference.
8
9use serde::Deserialize;
10
11/// One page of list results, normalized across every wire envelope.
12#[derive(Debug, Clone, PartialEq)]
13pub struct Page<T> {
14    /// The items.
15    pub data: Vec<T>,
16    /// Item count as reported by the daemon; synthesized as `data.len()` for
17    /// bare-array endpoints (and envelopes that omit `count`).
18    pub count: u64,
19    /// Total matching rows across pages, when the endpoint reports one (runs);
20    /// `None` otherwise.
21    pub total: Option<u64>,
22}
23
24impl<T> Page<T> {
25    /// Number of items in this page.
26    pub fn len(&self) -> usize {
27        self.data.len()
28    }
29
30    /// True when the page carries no items.
31    pub fn is_empty(&self) -> bool {
32        self.data.is_empty()
33    }
34
35    /// Iterate over the items.
36    pub fn iter(&self) -> std::slice::Iter<'_, T> {
37        self.data.iter()
38    }
39}
40
41impl<T> IntoIterator for Page<T> {
42    type Item = T;
43    type IntoIter = std::vec::IntoIter<T>;
44
45    fn into_iter(self) -> Self::IntoIter {
46        self.data.into_iter()
47    }
48}
49
50impl<'a, T> IntoIterator for &'a Page<T> {
51    type Item = &'a T;
52    type IntoIter = std::slice::Iter<'a, T>;
53
54    fn into_iter(self) -> Self::IntoIter {
55        self.data.iter()
56    }
57}
58
59impl<'de, T: Deserialize<'de>> Deserialize<'de> for Page<T> {
60    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61    where
62        D: serde::Deserializer<'de>,
63    {
64        #[derive(Deserialize)]
65        #[serde(untagged)]
66        enum Envelope<T> {
67            Wrapped {
68                data: Vec<T>,
69                #[serde(default)]
70                count: Option<u64>,
71                #[serde(default)]
72                total: Option<u64>,
73            },
74            Bare(Vec<T>),
75        }
76
77        Ok(match Envelope::<T>::deserialize(deserializer)? {
78            Envelope::Wrapped { data, count, total } => {
79                let len = data.len() as u64;
80                Page {
81                    data,
82                    count: count.unwrap_or(len),
83                    total,
84                }
85            }
86            Envelope::Bare(data) => {
87                let len = data.len() as u64;
88                Page {
89                    data,
90                    count: len,
91                    total: None,
92                }
93            }
94        })
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use serde_json::json;
102
103    #[test]
104    fn wrapped_envelope_with_count() {
105        let page: Page<serde_json::Value> =
106            serde_json::from_value(json!({"data": [{"id": 1}, {"id": 2}], "count": 2})).unwrap();
107        assert_eq!(page.len(), 2);
108        assert_eq!(page.count, 2);
109        assert_eq!(page.total, None);
110    }
111
112    #[test]
113    fn wrapped_envelope_with_total() {
114        let page: Page<serde_json::Value> =
115            serde_json::from_value(json!({"data": [{"id": 1}], "count": 1, "total": 41})).unwrap();
116        assert_eq!(page.count, 1);
117        assert_eq!(page.total, Some(41));
118    }
119
120    #[test]
121    fn bare_array_synthesizes_count() {
122        let page: Page<serde_json::Value> =
123            serde_json::from_value(json!([{"id": 1}, {"id": 2}, {"id": 3}])).unwrap();
124        assert_eq!(page.count, 3);
125        assert_eq!(page.total, None);
126    }
127}