Skip to main content

scope_cli/cli/tui/
vocab.rs

1//! # Completion Vocabulary
2//!
3//! The values that completion offers besides commands and flags: address-book
4//! labels, saved token aliases, venue IDs and chains. They load once when the
5//! TUI starts. A store that fails to load gives an empty list, so completion
6//! of commands and flags keeps working.
7
8use scope::config::Config;
9use scope::domain::address_book::AddressBook;
10use scope::market::{MarketVenue, VenueRegistry};
11use scope::tokens::TokenAliases;
12
13/// Dynamic completion values.
14#[derive(Debug, Clone, Default)]
15pub struct Vocab {
16    /// Address-book labels, without the `@`.
17    pub labels: Vec<String>,
18    /// Saved token alias symbols.
19    pub aliases: Vec<String>,
20    /// Every venue id `--venue` accepts: the registry venues, then DEX.
21    pub venues: Vec<String>,
22    /// Chain names.
23    pub chains: Vec<String>,
24}
25
26impl Vocab {
27    /// Loads the vocabulary from the user's stores.
28    pub fn load(config: &Config) -> Self {
29        let mut labels: Vec<String> = AddressBook::load(&config.data_dir())
30            .map(|b| b.addresses.into_iter().filter_map(|a| a.label).collect())
31            .unwrap_or_default();
32        labels.sort();
33        labels.dedup();
34        let mut aliases: Vec<String> = TokenAliases::load()
35            .list()
36            .into_iter()
37            .map(|t| t.symbol.clone())
38            .collect();
39        aliases.sort();
40        aliases.dedup();
41        let venues = VenueRegistry::load()
42            .map(|r| MarketVenue::ids(&r))
43            .unwrap_or_default();
44        Self {
45            labels,
46            aliases,
47            venues,
48            chains: scope::chains::registry::client_chain_ids()
49                .iter()
50                .map(|c| c.to_string())
51                .collect(),
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_load_with_missing_stores_still_has_chains() {
62        // Review Focus 3: no address book, no data dir. Completion of
63        // chains must still work.
64        let dir = tempfile::tempdir().unwrap();
65        let mut config = Config::default();
66        config.address_book.data_dir = Some(dir.path().join("nothing-here"));
67        let v = Vocab::load(&config);
68        assert!(v.labels.is_empty());
69        assert!(v.chains.iter().any(|c| c == "ethereum"));
70    }
71
72    #[test]
73    fn test_load_with_corrupt_address_book_gives_empty_labels() {
74        let dir = tempfile::tempdir().unwrap();
75        std::fs::write(dir.path().join("address_book.yaml"), "addresses: [unclosed").unwrap();
76        let mut config = Config::default();
77        config.address_book.data_dir = Some(dir.path().to_path_buf());
78        let v = Vocab::load(&config);
79        assert!(v.labels.is_empty());
80    }
81
82    #[test]
83    fn test_labels_come_from_the_address_book() {
84        let dir = tempfile::tempdir().unwrap();
85        std::fs::write(
86            dir.path().join("address_book.yaml"),
87            "addresses:\n  - address: '0x1'\n    label: cold\n    chain: ethereum\n    tags: []\n    added_at: 0\n",
88        )
89        .unwrap();
90        let mut config = Config::default();
91        config.address_book.data_dir = Some(dir.path().to_path_buf());
92        assert_eq!(Vocab::load(&config).labels, vec!["cold"]);
93    }
94}