Skip to main content

nextcloud_ocs/query/
query_string.rs

1use super::OcsFormat;
2use super::encoding::encode_component;
3
4/// Ordered query parameters for an OCS call.
5///
6/// OCS reads parameters by name, so order does not matter to the server, but
7/// insertion order is preserved here to keep a built query stable across runs
8/// and comparable in tests.
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
10pub struct Query {
11    params: Vec<(String, String)>,
12}
13
14impl Query {
15    /// Empty query.
16    pub const fn new() -> Self {
17        Self { params: Vec::new() }
18    }
19
20    /// Adds a parameter, replacing an existing one with the same name.
21    ///
22    /// Keys and values are percent-encoded when the query is built, so callers
23    /// pass raw text: a value containing `&` or `=` stays one value.
24    #[must_use]
25    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
26        let key = key.into();
27        let value = value.into();
28        match self.params.iter_mut().find(|(name, _)| name == &key) {
29            Some(entry) => entry.1 = value,
30            None => self.params.push((key, value)),
31        }
32        self
33    }
34
35    /// Selects the response format.
36    ///
37    /// Equivalent to `.param("format", format)`, kept as its own method so the
38    /// requirement for `format=json` is impossible to miss at a call site.
39    #[must_use]
40    pub fn format(self, format: OcsFormat) -> Self {
41        self.param(OcsFormat::PARAM, format.as_str())
42    }
43
44    /// Number of parameters held.
45    pub fn len(&self) -> usize {
46        self.params.len()
47    }
48
49    /// Whether no parameter is held.
50    pub fn is_empty(&self) -> bool {
51        self.params.is_empty()
52    }
53
54    /// Iterates over the parameters in insertion order.
55    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
56        self.params
57            .iter()
58            .map(|(key, value)| (key.as_str(), value.as_str()))
59    }
60
61    /// Renders the parameters as a query string, empty when there are none.
62    ///
63    /// The result starts with `?` so callers can concatenate it onto a path
64    /// without branching.
65    pub fn build(&self) -> String {
66        let mut out = String::new();
67        for (key, value) in &self.params {
68            out.push(if out.is_empty() { '?' } else { '&' });
69            out.push_str(&encode_component(key));
70            out.push('=');
71            out.push_str(&encode_component(value));
72        }
73        out
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::{OcsFormat, Query};
80
81    #[test]
82    fn empty_query_renders_nothing() {
83        let query = Query::new();
84        assert!(query.is_empty());
85        assert_eq!(query.build(), "");
86    }
87
88    #[test]
89    fn params_are_joined_with_ampersands() {
90        let query = Query::new()
91            .param("limit", "5")
92            .param("offset", "10")
93            .format(OcsFormat::Json);
94        assert_eq!(query.build(), "?limit=5&offset=10&format=json");
95        assert_eq!(query.len(), 3);
96    }
97
98    #[test]
99    fn xml_format_is_selectable() {
100        assert_eq!(Query::new().format(OcsFormat::Xml).build(), "?format=xml");
101    }
102
103    #[test]
104    fn repeated_param_replaces_the_earlier_value() {
105        let query = Query::new().param("limit", "5").param("limit", "50");
106        assert_eq!(query.len(), 1);
107        assert_eq!(query.build(), "?limit=50");
108    }
109
110    #[test]
111    fn values_are_percent_encoded() {
112        let query = Query::new().param("search", "a&b=c d");
113        assert_eq!(query.build(), "?search=a%26b%3Dc%20d");
114    }
115
116    #[test]
117    fn iterators_yield_raw_values() {
118        let query = Query::new().param("user", "a b");
119        let pairs: Vec<_> = query.iter().collect();
120        assert_eq!(pairs, vec![("user", "a b")]);
121    }
122}