Skip to main content

argot_cli/
types.rs

1use std::collections::HashMap;
2use std::ops::Add;
3
4#[cfg(feature = "serde")]
5use serde::{Serialize, Deserialize};
6#[cfg(feature = "serde")]
7use serde::ser::{SerializeStruct, Serializer};
8
9#[cfg(feature = "serde")]
10type SResult<S> = Result<<S as Serializer>::Ok, <S as Serializer>::Error>;
11
12#[cfg(feature = "serde")]
13fn serialize_text_entry<S>(default: &Option<String>, serializer: S) -> SResult<S>
14where
15    S: Serializer
16{
17    let name = "TextEntry";
18    if let Some(value) = default {
19        let mut entry = serializer.serialize_struct(name, 2)?;
20        entry.serialize_field("type", "text")?;
21        entry.serialize_field("default", value)?;
22        entry.end()
23    } else {
24        let mut entry = serializer.serialize_struct(name, 1)?;
25        entry.serialize_field("type", "text")?;
26        entry.end()
27    }
28}
29
30#[cfg(feature = "serde")]
31fn serialize_int_entry<S>(default: &Option<i64>, serializer: S) -> SResult<S>
32where
33    S: Serializer
34{
35    let name = "IntEntry";
36    if let Some(value) = default {
37        let mut entry = serializer.serialize_struct(name, 2)?;
38        entry.serialize_field("type", "int")?;
39        entry.serialize_field("default", value)?;
40        entry.end()
41    } else {
42        let mut entry = serializer.serialize_struct(name, 1)?;
43        entry.serialize_field("type", "int")?;
44        entry.end()
45    }
46}
47
48#[cfg(feature = "serde")]
49fn serialize_list_entry<S>(sep: &Option<String>, serializer: S) -> SResult<S>
50where
51    S: Serializer,
52{
53    let name = "ListEntry";
54    if let Some(value) = sep {
55        let mut entry = serializer.serialize_struct(name, 2)?;
56        entry.serialize_field("type", "list")?;
57        entry.serialize_field("sep", &value)?;
58        entry.end()
59    } else {
60        let mut entry = serializer.serialize_struct(name, 1)?;
61        entry.serialize_field("type", "list")?;
62        entry.end()
63    }
64}
65
66#[derive(Clone, Debug, PartialEq)]
67#[cfg_attr(
68    feature = "serde",
69    derive(Serialize, Deserialize),
70    serde(tag = "type", rename_all = "lowercase"))]
71pub enum ConfigEntry {
72    Flag,
73
74    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_text_entry"))]
75    Text { default: Option<String> },
76
77    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_int_entry"))]
78    Int { default: Option<i64> },
79
80    Count,
81
82    #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_list_entry"))]
83    List { sep: Option<String> },
84
85    Alias { target: String },
86}
87
88#[derive(Debug, PartialEq)]
89#[cfg_attr(feature = "serde", derive(Deserialize))]
90pub struct LabeledEntry {
91    pub option: String,
92
93    #[cfg_attr(feature = "serde", serde(flatten))]
94    pub entry: ConfigEntry,
95}
96
97#[derive(Debug, PartialEq)]
98#[cfg_attr(feature = "serde", derive(Deserialize), serde(untagged))]
99pub enum ConfigEntries {
100    Map(HashMap<String, ConfigEntry>),
101    List(Vec<LabeledEntry>),
102}
103
104impl ConfigEntries {
105    pub fn len(&self) -> usize {
106        match self {
107            Self::Map(map) => map.len(),
108            Self::List(list) => list.len(),
109        }
110    }
111
112    pub fn is_empty(&self) -> bool {
113        self.len() == 0
114    }
115}
116
117#[derive(Clone, Debug, PartialEq)]
118#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(untagged))]
119pub enum OptionValue {
120    #[cfg_attr(feature = "serde", serde(with = "serde_flag"))]
121    Flag,
122    Text(String),
123    Int(i64),
124    List(Vec<String>),
125}
126
127impl<T> Add<T> for OptionValue
128where
129    T: Into<i64>,
130{
131    type Output = Self;
132
133    fn add(self, other: T) -> Self {
134        match self {
135            OptionValue::Int(num) => OptionValue::Int(num + other.into()),
136            _ => {
137                panic!("attempt to add integer to non-int OptionValue variant");
138            },
139        }
140    }
141}
142
143#[cfg(feature = "serde")]
144mod serde_flag {
145    use std::fmt;
146    use serde::ser::Serializer;
147    use serde::de::{self, Deserializer, Visitor};
148
149    use super::SResult;
150
151    pub fn serialize<S>(serializer: S) -> SResult<S>
152    where
153        S: Serializer,
154    {
155        serializer.serialize_bool(true)
156    }
157
158    struct FlagVisitor;
159
160    impl<'de> Visitor<'de> for FlagVisitor {
161        type Value = ();
162
163        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
164            formatter.write_str("a boolean true")
165        }
166
167        fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
168        where
169            E: de::Error,
170        {
171            if value {
172                Ok(())
173            } else {
174                Err(E::custom("flag cannot be false"))
175            }
176        }
177    }
178
179    pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
180    where
181        D: Deserializer<'de>,
182    {
183        deserializer.deserialize_bool(FlagVisitor)
184    }
185}
186
187#[cfg(all(test, feature = "json"))]
188mod test_config_entry_serialize {
189    use super::*;
190    use serde_json::{json, to_value};
191
192    #[test]
193    fn serialize_config_entry_flag() {
194        let value = to_value(ConfigEntry::Flag).unwrap();
195        let expected = json!({ "type": "flag" });
196
197        assert_eq!(value, expected);
198    }
199
200    #[test]
201    fn serialize_config_entry_text_none() {
202        let value = to_value(ConfigEntry::Text { default: None }).unwrap();
203        let expected = json!({ "type": "text" });
204
205        assert_eq!(value, expected);
206    }
207
208    #[test]
209    fn serialize_config_entry_text_some() {
210        let value = to_value(ConfigEntry::Text {
211            default: Some("xdg-open".to_string()),
212        }).unwrap();
213        let expected = json!({ "type": "text", "default": "xdg-open" });
214
215        assert_eq!(value, expected);
216    }
217
218    #[test]
219    fn serialize_config_entry_int_none() {
220        let value = to_value(ConfigEntry::Int { default: None }).unwrap();
221        let expected = json!({ "type": "int" });
222
223        assert_eq!(value, expected);
224    }
225
226    #[test]
227    fn serialize_config_entry_int_some() {
228        let value = to_value(ConfigEntry::Int { default: Some(42) }).unwrap();
229        let expected = json!({ "type": "int", "default": 42 });
230
231        assert_eq!(value, expected);
232    }
233
234    #[test]
235    fn serialize_config_entry_count() {
236        let value = to_value(ConfigEntry::Count).unwrap();
237        let expected = json!({ "type": "count" });
238
239        assert_eq!(value, expected);
240    }
241
242    #[test]
243    fn serialize_config_entry_list_none() {
244        let value = to_value(ConfigEntry::List { sep: None }).unwrap();
245        let expected = json!({ "type": "list" });
246
247        assert_eq!(value, expected);
248    }
249
250    #[test]
251    fn serialize_config_entry_list_some() {
252        let value = to_value(ConfigEntry::List {
253            sep: Some(":".to_string()),
254        }).unwrap();
255        let expected = json!({ "type": "list", "sep": ":" });
256
257        assert_eq!(value, expected);
258    }
259
260    #[test]
261    fn serialize_config_entry_alias() {
262        let value = to_value(ConfigEntry::Alias {
263            target: "quiet".to_string(),
264        }).unwrap();
265        let expected = json!({ "type": "alias", "target": "quiet" });
266
267        assert_eq!(value, expected);
268    }
269}
270
271#[cfg(all(test, feature = "json"))]
272mod test_config_entry_deserialize {
273    use super::*;
274    use serde_json::{json, from_value};
275
276    #[test]
277    fn deserialize_config_entry_flag() {
278        let value = json!({ "type": "flag" });
279        let entry: ConfigEntry = from_value(value).unwrap();
280
281        assert_eq!(entry, ConfigEntry::Flag);
282    }
283
284    #[test]
285    fn deserialize_config_entry_text_none() {
286        let value = json!({ "type": "text" });
287        let entry: ConfigEntry = from_value(value).unwrap();
288
289        assert_eq!(entry, ConfigEntry::Text { default: None });
290    }
291
292    #[test]
293    fn deserialize_config_entry_text_some() {
294        let value = json!({ "type": "text", "default": "all" });
295        let entry: ConfigEntry = from_value(value).unwrap();
296
297        let default = Some("all".to_string());
298        assert_eq!(entry, ConfigEntry::Text { default });
299    }
300
301    #[test]
302    fn deserialize_config_entry_int_none() {
303        let value = json!({ "type": "int" });
304        let entry: ConfigEntry = from_value(value).unwrap();
305
306        assert_eq!(entry, ConfigEntry::Int { default: None });
307    }
308
309    #[test]
310    fn deserialize_config_entry_int_some() {
311        let value = json!({ "type": "int", "default": 12 });
312        let entry: ConfigEntry = from_value(value).unwrap();
313
314        let default = Some(12);
315        assert_eq!(entry, ConfigEntry::Int { default });
316    }
317
318    #[test]
319    fn deserialize_config_entry_count() {
320    }
321
322    #[test]
323    fn deserialize_config_entry_list_none() {
324        let value = json!({ "type": "list" });
325        let entry: ConfigEntry = from_value(value).unwrap();
326
327        assert_eq!(entry, ConfigEntry::List { sep: None });
328    }
329
330    #[test]
331    fn deserialize_config_entry_list_some() {
332        let value = json!({ "type": "list", "sep": ":" });
333        let entry: ConfigEntry = from_value(value).unwrap();
334
335        let sep = Some(":".to_string());
336        assert_eq!(entry, ConfigEntry::List { sep });
337    }
338
339    #[test]
340    fn deserialize_config_entry_alias() {
341        let value = json!({ "type": "alias", "target": "quiet" });
342        let entry: ConfigEntry = from_value(value).unwrap();
343
344        let target = "quiet".to_string();
345        assert_eq!(entry, ConfigEntry::Alias { target });
346    }
347
348    #[test]
349    fn deserialize_config_entries_object() {
350        let value = json!({
351            "quiet": { "type": "flag" },
352            "q": { "type": "alias", "target": "quiet" },
353            "verbose": { "type": "count" },
354            "v": { "type": "alias", "target": "verbose" },
355            "j": { "type": "int", "default": 0 },
356            "browser": { "type": "text" },
357            "hints": { "type": "list" },
358        });
359        let configs: HashMap<String, ConfigEntry> = from_value(value).unwrap();
360        let expected = HashMap::from([
361            ("quiet".to_string(), ConfigEntry::Flag),
362            ("q".to_string(), ConfigEntry::Alias { target: "quiet".to_string() }),
363            ("verbose".to_string(), ConfigEntry::Count),
364            ("v".to_string(), ConfigEntry::Alias { target: "verbose".to_string() }),
365            ("j".to_string(), ConfigEntry::Int { default: Some(0) }),
366            ("browser".to_string(), ConfigEntry::Text { default: None }),
367            ("hints".to_string(), ConfigEntry::List { sep: None }),
368        ]);
369
370        assert_eq!(configs, expected);
371    }
372
373    #[test]
374    fn deserialize_config_entries_array() {
375        let value = json!([
376            { "option": "quiet", "type": "flag" },
377            { "option": "q", "type": "alias", "target": "quiet" },
378            { "option": "verbose", "type": "count" },
379            { "option": "v", "type": "alias", "target": "verbose" },
380            { "option": "j", "type": "int", "default": 0 },
381            { "option": "browser", "type": "text" },
382            { "option": "hints", "type": "list" },
383        ]);
384        let configs: Vec<LabeledEntry> = from_value(value).unwrap();
385        let expected = vec![
386            LabeledEntry {
387                option: "quiet".to_string(),
388                entry: ConfigEntry::Flag
389            },
390            LabeledEntry {
391                option: "q".to_string(),
392                entry: ConfigEntry::Alias { target: "quiet".to_string() }
393            },
394            LabeledEntry {
395                option: "verbose".to_string(),
396                entry: ConfigEntry::Count
397            },
398            LabeledEntry {
399                option: "v".to_string(),
400                entry: ConfigEntry::Alias { target: "verbose".to_string() }
401            },
402            LabeledEntry {
403                option: "j".to_string(),
404                entry: ConfigEntry::Int { default: Some(0) }
405            },
406            LabeledEntry {
407                option: "browser".to_string(),
408                entry: ConfigEntry::Text { default: None }
409            },
410            LabeledEntry {
411                option: "hints".to_string(),
412                entry: ConfigEntry::List { sep: None }
413            },
414        ];
415
416        assert_eq!(configs, expected);
417    }
418
419    #[test]
420    fn deserialize_config_entries_object_into_wrapper() {
421        let value = json!({
422            "quiet": { "type": "flag" },
423            "q": { "type": "alias", "target": "quiet" },
424            "verbose": { "type": "count" },
425            "v": { "type": "alias", "target": "verbose" },
426            "j": { "type": "int", "default": 0 },
427            "browser": { "type": "text" },
428            "hints": { "type": "list" },
429        });
430
431        let configs: ConfigEntries = from_value(value).unwrap();
432        let map = HashMap::from([
433            ("quiet".to_string(), ConfigEntry::Flag),
434            ("q".to_string(), ConfigEntry::Alias { target: "quiet".to_string() }),
435            ("verbose".to_string(), ConfigEntry::Count),
436            ("v".to_string(), ConfigEntry::Alias { target: "verbose".to_string() }),
437            ("j".to_string(), ConfigEntry::Int { default: Some(0) }),
438            ("browser".to_string(), ConfigEntry::Text { default: None }),
439            ("hints".to_string(), ConfigEntry::List { sep: None }),
440        ]);
441        let expected = ConfigEntries::Map(map);
442
443        assert_eq!(configs, expected);
444    }
445
446    #[test]
447    fn deserialize_config_entries_array_into_wrapper() {
448        let value = json!([
449            { "option": "quiet", "type": "flag" },
450            { "option": "q", "type": "alias", "target": "quiet" },
451            { "option": "verbose", "type": "count" },
452            { "option": "v", "type": "alias", "target": "verbose" },
453            { "option": "j", "type": "int", "default": 0 },
454            { "option": "browser", "type": "text" },
455            { "option": "hints", "type": "list" },
456        ]);
457        let configs: ConfigEntries = from_value(value).unwrap();
458        let list = vec![
459            LabeledEntry {
460                option: "quiet".to_string(),
461                entry: ConfigEntry::Flag
462            },
463            LabeledEntry {
464                option: "q".to_string(),
465                entry: ConfigEntry::Alias { target: "quiet".to_string() }
466            },
467            LabeledEntry {
468                option: "verbose".to_string(),
469                entry: ConfigEntry::Count
470            },
471            LabeledEntry {
472                option: "v".to_string(),
473                entry: ConfigEntry::Alias { target: "verbose".to_string() }
474            },
475            LabeledEntry {
476                option: "j".to_string(),
477                entry: ConfigEntry::Int { default: Some(0) }
478            },
479            LabeledEntry {
480                option: "browser".to_string(),
481                entry: ConfigEntry::Text { default: None }
482            },
483            LabeledEntry {
484                option: "hints".to_string(),
485                entry: ConfigEntry::List { sep: None }
486            },
487        ];
488        let expected = ConfigEntries::List(list);
489
490        assert_eq!(configs, expected);
491    }
492}
493
494#[cfg(all(test, feature = "json"))]
495mod test_option_value_serialize {
496    use super::*;
497    use serde_json::{json, to_value};
498
499    #[test]
500    fn serialize_option_value_flag() {
501        let value = to_value(OptionValue::Flag).unwrap();
502        let expected = json!(true);
503
504        assert_eq!(value, expected);
505    }
506
507    #[test]
508    fn serialize_option_value_text() {
509        let value = to_value(OptionValue::Text("build".into())).unwrap();
510        let expected = json!("build");
511
512        assert_eq!(value, expected);
513    }
514
515    #[test]
516    fn serialize_option_value_int() {
517        let value = to_value(OptionValue::Int(123)).unwrap();
518        let expected = json!(123);
519
520        assert_eq!(value, expected);
521    }
522
523    #[test]
524    fn serialize_option_value_list() {
525        let value = to_value(OptionValue::List(vec![
526            "file1.txt".into(),
527            "file2.txt".into(),
528        ])).unwrap();
529        let expected = json!(["file1.txt", "file2.txt"]);
530
531        assert_eq!(value, expected);
532    }
533}
534
535#[cfg(all(test, feature = "json"))]
536mod test_option_value_deserialize {
537    use super::*;
538    use serde_json::{json, from_value};
539
540    #[test]
541    fn deserialize_option_value_flag() {
542        let input = json!(true);
543        let output: OptionValue = from_value(input).unwrap();
544        let expected = OptionValue::Flag;
545
546        assert_eq!(output, expected);
547    }
548
549    #[test]
550    fn deserialize_option_value_text() {
551        let input = json!("install");
552        let output: OptionValue = from_value(input).unwrap();
553        let expected = OptionValue::Text("install".to_string());
554
555        assert_eq!(output, expected);
556    }
557
558    #[test]
559    fn deserialize_option_value_int() {
560        let input = json!(7);
561        let output: OptionValue = from_value(input).unwrap();
562        let expected = OptionValue::Int(7);
563
564        assert_eq!(output, expected);
565    }
566
567    #[test]
568    fn deserialize_option_value_list() {
569        let input = json!(["all", "install"]);
570        let output: OptionValue = from_value(input).unwrap();
571        let expected = OptionValue::List(vec![
572            "all".to_string(),
573            "install".to_string(),
574        ]);
575
576        assert_eq!(output, expected);
577    }
578
579    #[test]
580    fn deserialize_option_values_object() {
581        let input = json!({
582            "verbose": 2,
583            "dry-run": true,
584            "browser": "chromium",
585            "hints": ["test", "ui"]
586        });
587        let output: HashMap<String, OptionValue> = from_value(input).unwrap();
588        let expected = HashMap::from([
589            ("verbose".to_string(), OptionValue::Int(2)),
590            ("dry-run".to_string(), OptionValue::Flag),
591            ("browser".to_string(), OptionValue::Text("chromium".to_string())),
592            (
593                "hints".to_string(),
594                OptionValue::List(vec!["test".to_string(), "ui".to_string()]),
595            ),
596        ]);
597
598        assert_eq!(output, expected);
599    }
600}