workspacer_either/
workspacer_either.rs

1crate::ix!();
2
3/// An enum representing either a “single crate” handle or an entire workspace.
4/// Use `SingleOrWorkspace::detect(...)` to try loading a workspace, and if it
5/// fails specifically with `WorkspaceError::ActuallyInSingleCrate {..}` we
6/// fallback to a single crate handle.
7pub enum SingleOrWorkspace {
8    Single(CrateHandle),
9    Workspace(Workspace<PathBuf, CrateHandle>),
10}
11
12impl SingleOrWorkspace {
13    /// Attempt to detect if `path` is a valid workspace. 
14    /// If so, return `SingleOrWorkspace::Workspace`.
15    /// If the error is `ActuallyInSingleCrate`, fallback to single crate.
16    /// Otherwise, return the original error.
17    #[instrument(level = "trace")]
18    pub async fn detect(path: &Path) -> Result<Self, WorkspaceError> {
19        // To handle relative vs. absolute paths, let's canonicalize if possible:
20        let path_canon = match tokio::fs::canonicalize(path).await {
21            Ok(abs) => abs,
22            Err(e) => {
23                error!("Could not canonicalize path={:?}: {:?}", path, e);
24                // fallback to the raw path anyway, or return an error
25                path.to_path_buf()
26            }
27        };
28
29        debug!("Attempting to interpret path='{}' as a workspace", path_canon.display());
30
31        match Workspace::<PathBuf, CrateHandle>::new(&path_canon).await {
32            Ok(ws) => {
33                debug!("Successfully loaded path='{}' as a workspace", path_canon.display());
34                Ok(SingleOrWorkspace::Workspace(ws))
35            }
36            Err(e) => {
37                match e {
38                    WorkspaceError::ActuallyInSingleCrate { .. } => {
39                        // Fallback: treat it as a single crate
40                        debug!("Falling back to single crate handle for path='{}'", path_canon.display());
41                        let ch = CrateHandle::new(&path_canon).await.map_err(|crate_err| {
42                            WorkspaceError::CrateError(crate_err)
43                        })?;
44                        Ok(SingleOrWorkspace::Single(ch))
45                    }
46                    other => {
47                        error!("Failed to interpret path='{}' as a workspace: {:?}", path_canon.display(), other);
48                        Err(other)
49                    }
50                }
51            }
52        }
53    }
54
55    /// Check that Git is clean (if you so desire). We do so by calling
56    /// `git status --porcelain` and verifying no output. This is the same
57    /// logic you'd do for either single crate or entire workspace, so there's
58    /// no difference inside here. 
59    #[instrument(level = "trace", skip(self))]
60    pub async fn ensure_git_clean(&self) -> Result<(), WorkspaceError> {
61        // Just call `git status --porcelain`
62        use std::process::Command;
63
64        debug!("Running git cleanliness check in single_or_workspace ...");
65        let output = Command::new("git")
66            .args(&["status", "--porcelain"])
67            .output()
68            .map_err(|io_err| {
69                error!("Failed to run `git status --porcelain`: {:?}", io_err);
70                WorkspaceError::GitError(GitError::FailedToRunGitStatusMakeSureGitIsInstalled)
71            })?;
72
73        if !output.status.success() {
74            let code = output.status.code().unwrap_or(-1);
75            error!(
76                "Command `git status --porcelain` exited with non-zero status code: {}",
77                code
78            );
79            return Err(WorkspaceError::GitError(
80                GitError::WorkingDirectoryIsNotCleanAborting,
81            ));
82        }
83
84        let stdout_str = String::from_utf8_lossy(&output.stdout);
85        if !stdout_str.trim().is_empty() {
86            error!("Git working directory is not clean:\n{}", stdout_str);
87            return Err(WorkspaceError::GitError(
88                GitError::WorkingDirectoryIsNotCleanAborting,
89            ));
90        }
91
92        info!("Git working directory is clean; proceeding.");
93        Ok(())
94    }
95
96    /// Validate integrity for either a single crate or a workspace.
97    #[instrument(level="trace", skip(self))]
98    pub async fn validate_integrity(&mut self) -> Result<(), WorkspaceError> {
99        match self {
100            SingleOrWorkspace::Single(ch) => {
101                ch.validate_integrity().await.map_err(|crate_err| {
102                    WorkspaceError::CrateError(crate_err)
103                })
104            }
105            SingleOrWorkspace::Workspace(ws) => {
106                ws.validate_integrity().await
107            }
108        }
109    }
110
111    /// A convenience function for hooking into your existing “run_with_crate”
112    /// or “run_with_workspace” approach. You pass a closure that takes either
113    /// a `&mut CrateHandle` or a `&mut Workspace`, depending on the variant.
114    ///
115    /// For example:
116    /// ```ignore
117    /// single_or_ws.run_operation(|handle_or_ws| async move {
118    ///     match handle_or_ws {
119    ///         SingleOrWorkspace::Single(ch) => {
120    ///             // do crate-specific code
121    ///         },
122    ///         SingleOrWorkspace::Workspace(ws) => {
123    ///             // do workspace code
124    ///         }
125    ///     }
126    ///     Ok(())
127    /// }).await?;
128    /// ```
129    #[instrument(level="trace", skip(self, operation))]
130    pub async fn run_operation<R, F>(&mut self, operation: F) -> Result<R, WorkspaceError>
131    where
132        // Our user-defined closure's return type is `Result<R, WorkspaceError>`.
133        R: Send + 'static,
134
135        // We define an async closure trait bound:
136        F: for<'a> FnOnce(
137                &'a mut SingleOrWorkspace
138            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<R, WorkspaceError>> + Send + 'a>>
139          + Send
140          + 'static
141    {
142        // Just call it directly:
143        let fut = operation(self);
144        fut.await
145    }
146}