Skip to main content

difflore_cli/runtime/
context.rs

1use std::path::PathBuf;
2
3use difflore_core::SqlitePool;
4use difflore_core::cloud::client::CloudClient;
5use tokio::sync::OnceCell;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum OutputMode {
9    Json,
10    Text,
11}
12
13impl OutputMode {
14    pub const fn from_json_flag(json: bool) -> Self {
15        if json { Self::Json } else { Self::Text }
16    }
17}
18
19pub struct CommandContext {
20    pub db: SqlitePool,
21    pub project: PathBuf,
22    pub mode: OutputMode,
23    cloud_cell: OnceCell<CloudClient>,
24}
25
26impl CommandContext {
27    pub async fn new(mode: OutputMode) -> Self {
28        let db = crate::commands::util::init_db().await;
29        let project = difflore_core::paths::current_project_root();
30        Self {
31            db,
32            project,
33            mode,
34            cloud_cell: OnceCell::new(),
35        }
36    }
37
38    /// Lazily construct the cloud client on first access. `CloudClient`
39    /// itself is always constructible; a missing/unconfigured token
40    /// surfaces as `client.token.is_none()`.
41    pub async fn cloud(&self) -> &CloudClient {
42        self.cloud_cell
43            .get_or_init(|| async { CloudClient::create().await })
44            .await
45    }
46
47    pub fn json(&self) -> bool {
48        self.mode == OutputMode::Json
49    }
50}
51
52#[cfg(test)]
53#[allow(unsafe_code)] // reason: `env::set_var` is unsafe in 2024 edition; SAFETY comment documents the OnceLock invariant.
54mod tests {
55    use super::*;
56    use std::sync::OnceLock;
57    use tempfile::TempDir;
58
59    /// Mirror `commands::rules::mod::ensure_test_home`: pin `DIFFLORE_HOME`
60    /// to a single tempdir for the whole test process so all `init_db`
61    /// callers share the same on-disk state without racing on env writes.
62    fn ensure_test_home() {
63        static HOME: OnceLock<TempDir> = OnceLock::new();
64        HOME.get_or_init(|| {
65            let dir = TempDir::new().expect("create runtime test home tempdir");
66            // SAFETY: OnceLock guarantees this closure runs once per
67            // process; the env var is never removed afterwards.
68            unsafe {
69                std::env::set_var("DIFFLORE_HOME", dir.path());
70            }
71            dir
72        });
73    }
74
75    #[tokio::test]
76    async fn new_context_initialises_db_and_resolves_project_root() {
77        ensure_test_home();
78        let ctx = CommandContext::new(OutputMode::Text).await;
79        let row: (i64,) = sqlx::query_as("SELECT 1")
80            .fetch_one(&ctx.db)
81            .await
82            .expect("trivial select must succeed");
83        assert_eq!(row.0, 1);
84        let expected = difflore_core::paths::current_project_root();
85        assert_eq!(ctx.project, expected);
86        assert!(!ctx.json());
87    }
88
89    #[tokio::test]
90    async fn json_mode_reports_json() {
91        ensure_test_home();
92        let ctx = CommandContext::new(OutputMode::Json).await;
93        assert!(ctx.json());
94    }
95
96    #[tokio::test]
97    async fn cloud_is_lazy_and_memoised() {
98        ensure_test_home();
99        let ctx = CommandContext::new(OutputMode::Text).await;
100        let first = std::ptr::from_ref(ctx.cloud().await);
101        let second = std::ptr::from_ref(ctx.cloud().await);
102        assert_eq!(first, second, "second cloud() must hit the OnceCell");
103    }
104
105    #[test]
106    fn output_mode_from_json_flag() {
107        assert_eq!(OutputMode::from_json_flag(true), OutputMode::Json);
108        assert_eq!(OutputMode::from_json_flag(false), OutputMode::Text);
109    }
110}