Skip to main content

vfa_tui/models/
workspace.rs

1//! Workspace registry models — entries, resolved state, and status.
2
3use serde::{Deserialize, Serialize};
4
5/// A single entry from the workspace registry TOML (`[[workspace]]` table).
6#[derive(Debug, Clone, Deserialize, Serialize)]
7pub struct WorkspaceEntry {
8    /// Path to the workspace root — supports `$HOME`/`$VAR` expansion.
9    pub path: String,
10    /// Human-readable name; defaults to the directory basename when absent.
11    pub name: Option<String>,
12    /// Owning team identifier.
13    pub team: Option<String>,
14    /// Free-form classification tags (e.g. `["production", "pci"]`).
15    pub tags: Option<Vec<String>>,
16    /// Per-workspace policy overrides stored as a raw TOML value.
17    pub policy_overrides: Option<toml::Value>,
18}
19
20/// A `WorkspaceEntry` after path expansion and filesystem validation.
21#[derive(Debug, Clone)]
22pub struct ResolvedWorkspace {
23    /// Canonicalized absolute path to the workspace root.
24    pub canonical_path: std::path::PathBuf,
25    /// Effective name (from entry or derived from basename).
26    pub name: String,
27    /// Owning team, if specified.
28    pub team: Option<String>,
29    /// Classification tags (empty if none were specified).
30    pub tags: Vec<String>,
31    /// Current reachability / scan status of this workspace.
32    pub status: WorkspaceStatus,
33}
34
35/// Reachability and scan status of a resolved workspace.
36#[derive(Debug, Clone, PartialEq)]
37pub enum WorkspaceStatus {
38    /// Workspace path exists and is accessible.
39    Available,
40    /// Workspace path is inaccessible; carries the reason.
41    Unavailable(String),
42    /// A background scan is currently in progress.
43    Scanning,
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn workspace_entry_round_trip() {
52        let entry = WorkspaceEntry {
53            path: "$HOME/repos/payment-service".to_string(),
54            name: Some("payment-service".to_string()),
55            team: Some("payments".to_string()),
56            tags: Some(vec!["production".to_string(), "pci".to_string()]),
57            policy_overrides: None,
58        };
59
60        let json = serde_json::to_string(&entry).expect("serialize");
61        let decoded: WorkspaceEntry = serde_json::from_str(&json).expect("deserialize");
62
63        assert_eq!(decoded.path, entry.path);
64        assert_eq!(decoded.name, entry.name);
65        assert_eq!(decoded.team, entry.team);
66        assert_eq!(decoded.tags, entry.tags);
67    }
68
69    #[test]
70    fn workspace_entry_minimal_fields() {
71        let json = r#"{"path": "/home/user/foo"}"#;
72        let entry: WorkspaceEntry = serde_json::from_str(json).expect("deserialize");
73        assert_eq!(entry.path, "/home/user/foo");
74        assert!(entry.name.is_none());
75        assert!(entry.team.is_none());
76        assert!(entry.tags.is_none());
77        assert!(entry.policy_overrides.is_none());
78    }
79
80    #[test]
81    fn workspace_status_equality() {
82        assert_eq!(WorkspaceStatus::Available, WorkspaceStatus::Available);
83        assert_eq!(
84            WorkspaceStatus::Unavailable("no such path".to_string()),
85            WorkspaceStatus::Unavailable("no such path".to_string()),
86        );
87        assert_ne!(WorkspaceStatus::Available, WorkspaceStatus::Scanning);
88    }
89
90    #[test]
91    fn resolved_workspace_debug_clone() {
92        let rw = ResolvedWorkspace {
93            canonical_path: std::path::PathBuf::from("/home/user/foo"),
94            name: "foo".to_string(),
95            team: None,
96            tags: vec![],
97            status: WorkspaceStatus::Available,
98        };
99        let cloned = rw.clone();
100        assert_eq!(cloned.name, "foo");
101        // Ensure Debug doesn't panic.
102        let _ = format!("{:?}", rw);
103    }
104}