Skip to main content

lingxia_shell/
activator.rs

1use crate::{ShellError, ShellResult};
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase")]
7pub struct ShellActivator {
8    pub id: String,
9    pub label: String,
10    pub icon: String,
11    #[serde(default)]
12    pub disabled: bool,
13}
14
15impl ShellActivator {
16    pub fn validate(mut self) -> ShellResult<Self> {
17        self.id = required(self.id, ShellError::EmptyActivatorId)?;
18        self.label = required_field(self.label, "label")?;
19        self.icon = required_field(self.icon, "icon")?;
20        Ok(self)
21    }
22}
23
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct ShellActivatorUpdate {
26    pub label: Option<String>,
27    pub icon: Option<String>,
28    pub disabled: Option<bool>,
29}
30
31impl ShellActivatorUpdate {
32    fn validate(mut self, id: &str) -> ShellResult<Self> {
33        if self.label.is_none() && self.icon.is_none() && self.disabled.is_none() {
34            return Err(ShellError::EmptyActivatorUpdate { id: id.to_string() });
35        }
36        self.label = optional(self.label, "label")?;
37        self.icon = optional(self.icon, "icon")?;
38        Ok(self)
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct ResolvedShellActivator {
45    pub id: String,
46    pub label: String,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub icon_path: Option<String>,
49    pub disabled: bool,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct ActivatorCollection {
54    generation: u64,
55    declared: bool,
56    items: Vec<ShellActivator>,
57}
58
59impl ActivatorCollection {
60    pub fn generation(&self) -> u64 {
61        self.generation
62    }
63
64    pub fn declared(&self) -> bool {
65        self.declared
66    }
67
68    pub fn items(&self) -> &[ShellActivator] {
69        &self.items
70    }
71
72    pub fn replace(&mut self, items: Vec<ShellActivator>) -> ShellResult<()> {
73        let items = validate_generation(items)?;
74        self.items = items;
75        self.declared = true;
76        self.generation = self.generation.wrapping_add(1);
77        Ok(())
78    }
79
80    pub fn update(&mut self, id: &str, patch: ShellActivatorUpdate) -> ShellResult<()> {
81        let id = id.trim();
82        if id.is_empty() {
83            return Err(ShellError::EmptyActivatorId);
84        }
85        let patch = patch.validate(id)?;
86        let Some(item) = self.items.iter_mut().find(|item| item.id == id) else {
87            return Err(ShellError::ActivatorNotFound { id: id.to_string() });
88        };
89        if let Some(label) = patch.label {
90            item.label = label;
91        }
92        if let Some(icon) = patch.icon {
93            item.icon = icon;
94        }
95        if let Some(disabled) = patch.disabled {
96            item.disabled = disabled;
97        }
98        self.declared = true;
99        self.generation = self.generation.wrapping_add(1);
100        Ok(())
101    }
102
103    pub fn remove(&mut self, id: &str) -> ShellResult<()> {
104        let id = id.trim();
105        if id.is_empty() {
106            return Err(ShellError::EmptyActivatorId);
107        }
108        let before = self.items.len();
109        self.items.retain(|item| item.id != id);
110        if self.items.len() == before {
111            return Err(ShellError::ActivatorNotFound { id: id.to_string() });
112        }
113        self.declared = true;
114        self.generation = self.generation.wrapping_add(1);
115        Ok(())
116    }
117
118    pub fn clear(&mut self) {
119        self.items.clear();
120        self.declared = true;
121        self.generation = self.generation.wrapping_add(1);
122    }
123}
124
125fn required(value: String, error: ShellError) -> ShellResult<String> {
126    let value = value.trim();
127    if value.is_empty() {
128        Err(error)
129    } else {
130        Ok(value.to_string())
131    }
132}
133
134fn required_field(value: String, field: &'static str) -> ShellResult<String> {
135    let value = value.trim();
136    if value.is_empty() {
137        Err(ShellError::EmptyActivatorField { field })
138    } else {
139        Ok(value.to_string())
140    }
141}
142
143fn optional(value: Option<String>, field: &'static str) -> ShellResult<Option<String>> {
144    value.map(|value| required_field(value, field)).transpose()
145}
146
147fn validate_generation(items: Vec<ShellActivator>) -> ShellResult<Vec<ShellActivator>> {
148    let mut ids = HashSet::with_capacity(items.len());
149    items
150        .into_iter()
151        .map(ShellActivator::validate)
152        .map(|result| {
153            let item = result?;
154            if !ids.insert(item.id.clone()) {
155                return Err(ShellError::DuplicateActivatorId { id: item.id });
156            }
157            Ok(item)
158        })
159        .collect()
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn activator(id: &str) -> ShellActivator {
167        ShellActivator {
168            id: id.to_string(),
169            label: format!("Label {id}"),
170            icon: "icons/activator.svg".to_string(),
171            disabled: false,
172        }
173    }
174
175    #[test]
176    fn replace_is_atomic_when_a_later_item_is_invalid() {
177        let mut state = ActivatorCollection::default();
178        state.replace(vec![activator("chat")]).unwrap();
179        let before = state.clone();
180
181        let result = state.replace(vec![activator("ok"), activator("")]);
182
183        assert_eq!(result, Err(ShellError::EmptyActivatorId));
184        assert_eq!(state, before);
185    }
186
187    #[test]
188    fn clear_is_an_explicit_empty_declaration() {
189        let mut state = ActivatorCollection::default();
190        state.clear();
191
192        assert!(state.declared());
193        assert!(state.items().is_empty());
194    }
195
196    #[test]
197    fn label_and_icon_are_required() {
198        let mut missing_label = activator("sync");
199        missing_label.label.clear();
200        assert_eq!(
201            missing_label.validate(),
202            Err(ShellError::EmptyActivatorField { field: "label" })
203        );
204
205        let mut missing_icon = activator("sync");
206        missing_icon.icon.clear();
207        assert_eq!(
208            missing_icon.validate(),
209            Err(ShellError::EmptyActivatorField { field: "icon" })
210        );
211    }
212
213    #[test]
214    fn stable_ids_are_unique() {
215        let mut state = ActivatorCollection::default();
216        let result = state.replace(vec![activator("same"), activator("same")]);
217
218        assert_eq!(
219            result,
220            Err(ShellError::DuplicateActivatorId {
221                id: "same".to_string()
222            })
223        );
224    }
225}