Skip to main content

cpdb_rs/
options.rs

1//! Printer options (capabilities) returned by `GetAllOptions`.
2//!
3//! [`OptionsCollection`] provides an owned, framework-agnostic snapshot
4//! of a printer's supported settings (copies, duplex, color mode, etc.).
5
6/// A single printer option with its supported choices.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct OptionInfo {
9    /// The option name, e.g. `"copies"` or `"sides"`.
10    pub name: String,
11    /// The default value as reported by the backend.
12    pub default_value: String,
13    /// The option group (e.g. `"General"`), or an empty string when unset.
14    pub group: String,
15    /// All values the printer supports for this option.
16    pub supported_values: Vec<String>,
17}
18
19/// An owned snapshot of every option in a `cpdb_options_t`.
20///
21/// Built from D-Bus `GetAllOptions` responses (zbus backend) or from
22/// C `cpdb_options_t` pointers (FFI backend). After construction, no raw
23/// pointers are held - the collection is freely movable and cloneable.
24///
25/// # Example
26///
27/// ```rust
28/// use cpdb_rs::options::{OptionsCollection, OptionInfo};
29///
30/// let col = OptionsCollection {
31///     options: vec![OptionInfo {
32///         name: "copies".to_string(),
33///         default_value: "1".to_string(),
34///         group: "General".to_string(),
35///         supported_values: vec!["1".to_string(), "2".to_string()],
36///     }],
37/// };
38/// assert_eq!(col.get("copies").unwrap().default_value, "1");
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct OptionsCollection {
42    /// Every option discovered, in iteration order of the underlying
43    /// hash table (which itself is implementation-defined).
44    pub options: Vec<OptionInfo>,
45}
46
47impl OptionsCollection {
48    /// Builds an `OptionsCollection` from the D-Bus response tuples returned
49    /// by [`crate::proxy::PrintBackendProxy::get_all_options()`].
50    #[cfg(feature = "zbus-backend")]
51    pub fn from_dbus(raw: Vec<crate::proxy::RawOption>) -> Self {
52        let options = raw
53            .into_iter()
54            .map(|r| OptionInfo {
55                name: r.option_name,
56                group: r.group_name,
57                default_value: r.default_value,
58                supported_values: r.supported_values.into_iter().map(|(s,)| s).collect(),
59            })
60            .collect();
61        Self { options }
62    }
63
64    /// Returns the number of options in this collection.
65    #[inline]
66    pub fn len(&self) -> usize {
67        self.options.len()
68    }
69
70    /// Returns `true` if this collection has no options.
71    #[inline]
72    pub fn is_empty(&self) -> bool {
73        self.options.is_empty()
74    }
75
76    /// Finds an option by name (linear search).
77    pub fn get(&self, name: &str) -> Option<&OptionInfo> {
78        self.options.iter().find(|o| o.name == name)
79    }
80
81    /// Returns an iterator over all options.
82    pub fn iter(&self) -> impl Iterator<Item = &OptionInfo> {
83        self.options.iter()
84    }
85
86    /// Returns deduplicated group names of all options in their iteration order,
87    /// skipping empty group names.
88    pub fn groups(&self) -> Vec<&str> {
89        let mut seen = std::collections::HashSet::new();
90        let mut result = Vec::new();
91        for opt in &self.options {
92            if !opt.group.is_empty() && seen.insert(opt.group.as_str()) {
93                result.push(opt.group.as_str());
94            }
95        }
96        result
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn empty_collection_helpers() {
106        let col = OptionsCollection::default();
107        assert!(col.is_empty());
108        assert_eq!(col.len(), 0);
109        assert!(col.get("copies").is_none());
110        assert!(col.groups().is_empty());
111    }
112
113    #[test]
114    fn collection_get_finds_by_name() {
115        let col = OptionsCollection {
116            options: vec![
117                OptionInfo {
118                    name: "copies".to_string(),
119                    default_value: "1".to_string(),
120                    group: "General".to_string(),
121                    supported_values: vec!["1".to_string(), "2".to_string()],
122                },
123                OptionInfo {
124                    name: "sides".to_string(),
125                    default_value: "one-sided".to_string(),
126                    group: "General".to_string(),
127                    supported_values: vec![
128                        "one-sided".to_string(),
129                        "two-sided-long-edge".to_string(),
130                    ],
131                },
132            ],
133        };
134        let found = col.get("sides");
135        assert_eq!(found.unwrap().default_value, "one-sided");
136        assert!(col.get("nonexistent").is_none());
137    }
138
139    #[test]
140    fn collection_len_and_iter() {
141        let col = OptionsCollection {
142            options: vec![
143                OptionInfo {
144                    name: "a".into(),
145                    default_value: String::new(),
146                    group: String::new(),
147                    supported_values: vec![],
148                },
149                OptionInfo {
150                    name: "b".into(),
151                    default_value: String::new(),
152                    group: String::new(),
153                    supported_values: vec![],
154                },
155            ],
156        };
157        assert_eq!(col.len(), 2);
158        assert_eq!(col.iter().count(), 2);
159    }
160
161    #[test]
162    fn collection_groups() {
163        let col = OptionsCollection {
164            options: vec![
165                OptionInfo {
166                    name: "copies".to_string(),
167                    default_value: "1".to_string(),
168                    group: "General".to_string(),
169                    supported_values: vec![],
170                },
171                OptionInfo {
172                    name: "media".to_string(),
173                    default_value: "A4".to_string(),
174                    group: "Page Setup".to_string(),
175                    supported_values: vec![],
176                },
177                OptionInfo {
178                    name: "sides".to_string(),
179                    default_value: "one-sided".to_string(),
180                    group: "General".to_string(),
181                    supported_values: vec![],
182                },
183                OptionInfo {
184                    name: "fit-to-page".to_string(),
185                    default_value: "false".to_string(),
186                    group: "".to_string(),
187                    supported_values: vec![],
188                },
189            ],
190        };
191        assert_eq!(col.groups(), vec!["General", "Page Setup"]);
192    }
193}