Skip to main content

cossh/inventory/
model.rs

1//! Inventory domain models.
2
3use std::collections::BTreeMap;
4use std::convert::Infallible;
5use std::fmt;
6use std::path::PathBuf;
7use std::str::FromStr;
8
9/// Stable folder identifier used by the TUI tree.
10pub type FolderId = usize;
11
12/// Arbitrary SSH option map (`option_name -> values`).
13pub type SshOptionMap = BTreeMap<String, Vec<String>>;
14
15/// Inventory-level connection protocol.
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
17pub enum ConnectionProtocol {
18    /// OpenSSH launch.
19    #[default]
20    Ssh,
21    /// FreeRDP launch.
22    Rdp,
23    /// Preserved unknown protocol string.
24    Other(String),
25}
26
27impl ConnectionProtocol {
28    fn parse(value: &str) -> Self {
29        match value.trim().to_ascii_lowercase().as_str() {
30            "" | "ssh" => Self::Ssh,
31            "rdp" => Self::Rdp,
32            other => Self::Other(other.to_string()),
33        }
34    }
35
36    pub fn as_str(&self) -> &str {
37        match self {
38            Self::Ssh => "ssh",
39            Self::Rdp => "rdp",
40            Self::Other(value) => value.as_str(),
41        }
42    }
43
44    pub fn display_name(&self) -> &str {
45        match self {
46            Self::Ssh => "SSH",
47            Self::Rdp => "RDP",
48            Self::Other(value) => value.as_str(),
49        }
50    }
51}
52
53impl From<&str> for ConnectionProtocol {
54    fn from(value: &str) -> Self {
55        Self::parse(value)
56    }
57}
58
59impl FromStr for ConnectionProtocol {
60    type Err = Infallible;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        Ok(Self::parse(s))
64    }
65}
66
67impl fmt::Display for ConnectionProtocol {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str(self.as_str())
70    }
71}
72
73/// SSH-specific inventory options.
74#[derive(Debug, Clone, Default)]
75pub struct SshHostOptions {
76    /// One or more identity files.
77    pub identity_files: Vec<String>,
78    /// Equivalent to SSH `IdentitiesOnly`.
79    pub identities_only: Option<bool>,
80    /// SSH `ProxyJump`.
81    pub proxy_jump: Option<String>,
82    /// SSH `ProxyCommand`.
83    pub proxy_command: Option<String>,
84    /// SSH `ForwardAgent`.
85    pub forward_agent: Option<String>,
86    /// SSH `LocalForward` entries.
87    pub local_forward: Vec<String>,
88    /// SSH `RemoteForward` entries.
89    pub remote_forward: Vec<String>,
90    /// Additional SSH options not promoted to first-class fields.
91    pub extra_options: SshOptionMap,
92}
93
94/// RDP-specific inventory options.
95#[derive(Debug, Clone, Default)]
96pub struct RdpHostOptions {
97    /// Optional RDP domain.
98    pub domain: Option<String>,
99    /// Additional FreeRDP args.
100    pub args: Vec<String>,
101}
102
103/// Fully normalized host record loaded from inventory files.
104#[derive(Debug, Clone)]
105pub struct InventoryHost {
106    /// User-facing alias.
107    pub name: String,
108    /// Optional description shown in UI.
109    pub description: Option<String>,
110    /// Launch protocol.
111    pub protocol: ConnectionProtocol,
112    /// Destination hostname or IP.
113    pub host: String,
114    /// Optional login user.
115    pub user: Option<String>,
116    /// Optional destination port.
117    pub port: Option<u16>,
118    /// Optional runtime profile.
119    pub profile: Option<String>,
120    /// Optional vault entry name.
121    pub vault_pass: Option<String>,
122    /// Whether to hide this host from runtime host lists.
123    pub hidden: bool,
124    /// SSH-specific options.
125    pub ssh: SshHostOptions,
126    /// RDP-specific options.
127    pub rdp: RdpHostOptions,
128    /// Source inventory file where this host was loaded.
129    pub source_file: PathBuf,
130    /// Folder path from root to this host.
131    pub source_folder_path: Vec<String>,
132}
133
134impl InventoryHost {
135    /// Construct a host with SSH defaults using `name` as alias and host.
136    pub fn new(name: String) -> Self {
137        Self {
138            host: name.clone(),
139            name,
140            description: None,
141            protocol: ConnectionProtocol::Ssh,
142            user: None,
143            port: None,
144            profile: None,
145            vault_pass: None,
146            hidden: false,
147            ssh: SshHostOptions::default(),
148            rdp: RdpHostOptions::default(),
149            source_file: PathBuf::new(),
150            source_folder_path: Vec::new(),
151        }
152    }
153}
154
155/// Tree folder node used by the TUI tree.
156#[derive(Debug, Clone)]
157pub struct TreeFolder {
158    /// Stable folder id.
159    pub id: FolderId,
160    /// Folder display name.
161    pub name: String,
162    /// Source file path represented by this folder.
163    pub path: PathBuf,
164    /// Nested folders.
165    pub children: Vec<TreeFolder>,
166    /// Indices into [`InventoryTreeModel::hosts`].
167    pub host_indices: Vec<usize>,
168}
169
170/// Parsed inventory data and folder tree.
171#[derive(Debug, Clone)]
172pub struct InventoryTreeModel {
173    /// Folder tree rooted at the main inventory file.
174    pub root: TreeFolder,
175    /// Flattened hosts in tree traversal order.
176    pub hosts: Vec<InventoryHost>,
177}
178
179impl InventoryTreeModel {
180    pub(super) fn empty(root_path: PathBuf) -> Self {
181        let root_name = root_path
182            .file_name()
183            .and_then(|segment| segment.to_str())
184            .unwrap_or("cossh-inventory.yaml")
185            .to_string();
186        Self {
187            root: TreeFolder {
188                id: 0,
189                name: root_name,
190                path: root_path,
191                children: Vec::new(),
192                host_indices: Vec::new(),
193            },
194            hosts: Vec::new(),
195        }
196    }
197}
198
199#[derive(Debug, Clone, Default)]
200pub(super) struct ParsedInventoryDocument {
201    pub include: Vec<String>,
202    pub inventory: Vec<InventoryNodeRaw>,
203}
204
205#[derive(Debug, Clone)]
206pub(super) enum InventoryNodeRaw {
207    Host(Box<InventoryHostRaw>),
208    Folder { name: String, items: Vec<InventoryNodeRaw> },
209}
210
211#[derive(Debug, Clone, Default)]
212pub(super) struct InventoryHostRaw {
213    pub name: String,
214    pub description: Option<String>,
215    pub protocol: ConnectionProtocol,
216    pub host: Option<String>,
217    pub user: Option<String>,
218    pub port: Option<u16>,
219    pub profile: Option<String>,
220    pub vault_pass: Option<String>,
221    pub hidden: bool,
222    pub identity_files: Vec<String>,
223    pub identities_only: Option<bool>,
224    pub proxy_jump: Option<String>,
225    pub proxy_command: Option<String>,
226    pub forward_agent: Option<String>,
227    pub local_forward: Vec<String>,
228    pub remote_forward: Vec<String>,
229    pub ssh_options: SshOptionMap,
230    pub rdp_domain: Option<String>,
231    pub rdp_args: Vec<String>,
232}
233
234#[cfg(test)]
235#[path = "../test/inventory/model.rs"]
236mod tests;