Skip to main content

rustauth_cli/
workspace.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use cargo_metadata::{Metadata, MetadataCommand};
6use serde::Serialize;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum WorkspaceError {
11    #[error("failed to inspect Cargo metadata: {0}")]
12    Metadata(#[from] cargo_metadata::Error),
13    #[error("failed to run {program}: {source}")]
14    Command {
15        program: String,
16        source: std::io::Error,
17    },
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct WorkspaceInfo {
22    pub root: PathBuf,
23    pub packages: Vec<PackageInfo>,
24    pub detected_frameworks: Vec<DetectedItem>,
25    pub detected_databases: Vec<DetectedItem>,
26}
27
28#[derive(Debug, Clone, Serialize)]
29pub struct PackageInfo {
30    pub name: String,
31    pub version: String,
32    pub dependencies: Vec<String>,
33    pub features: BTreeMap<String, Vec<String>>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct DetectedItem {
38    pub name: String,
39    pub confidence: DetectionConfidence,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "lowercase")]
44pub enum DetectionConfidence {
45    High,
46    Medium,
47    Low,
48}
49
50pub fn inspect(cwd: &Path) -> Result<WorkspaceInfo, WorkspaceError> {
51    let metadata = MetadataCommand::new().current_dir(cwd).no_deps().exec()?;
52    Ok(WorkspaceInfo {
53        root: metadata.workspace_root.as_std_path().to_path_buf(),
54        packages: package_info(&metadata),
55        detected_frameworks: detect_frameworks(&metadata),
56        detected_databases: detect_databases(&metadata),
57    })
58}
59
60pub fn command_version(program: &str) -> Result<String, WorkspaceError> {
61    let output = Command::new(program)
62        .arg("--version")
63        .output()
64        .map_err(|source| WorkspaceError::Command {
65            program: program.to_owned(),
66            source,
67        })?;
68    if output.status.success() {
69        Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
70    } else {
71        Ok("not available".to_owned())
72    }
73}
74
75fn package_info(metadata: &Metadata) -> Vec<PackageInfo> {
76    metadata
77        .packages
78        .iter()
79        .map(|package| PackageInfo {
80            name: package.name.clone(),
81            version: package.version.to_string(),
82            dependencies: package
83                .dependencies
84                .iter()
85                .map(|dependency| dependency.name.clone())
86                .collect(),
87            features: package.features.clone(),
88        })
89        .collect()
90}
91
92fn dependency_names(metadata: &Metadata) -> BTreeSet<String> {
93    metadata
94        .packages
95        .iter()
96        .flat_map(|package| {
97            package
98                .dependencies
99                .iter()
100                .map(|dependency| dependency.name.clone())
101        })
102        .collect()
103}
104
105fn package_names(metadata: &Metadata) -> BTreeSet<String> {
106    metadata
107        .packages
108        .iter()
109        .map(|package| package.name.clone())
110        .collect()
111}
112
113fn has_dep_or_package(metadata: &Metadata, name: &str) -> bool {
114    let deps = dependency_names(metadata);
115    let packages = package_names(metadata);
116    deps.contains(name) || packages.contains(name)
117}
118
119fn detect_frameworks(metadata: &Metadata) -> Vec<DetectedItem> {
120    let mut frameworks = Vec::new();
121    let has_axum = has_dep_or_package(metadata, "axum");
122    let has_rustauth_axum = has_dep_or_package(metadata, "rustauth-axum");
123    if has_axum && has_rustauth_axum {
124        frameworks.push(detected("axum", DetectionConfidence::High));
125    } else if has_axum {
126        frameworks.push(detected("axum", DetectionConfidence::Medium));
127    }
128    let has_actix_web = has_dep_or_package(metadata, "actix-web");
129    let has_rustauth_actix_web = has_dep_or_package(metadata, "rustauth-actix-web");
130    if has_actix_web && has_rustauth_actix_web {
131        frameworks.push(detected("actix-web", DetectionConfidence::High));
132    } else if has_actix_web {
133        frameworks.push(detected("actix-web", DetectionConfidence::Medium));
134    }
135    for framework in ["rocket", "poem", "warp"] {
136        if has_dep_or_package(metadata, framework) {
137            frameworks.push(detected(framework, DetectionConfidence::Low));
138        }
139    }
140    frameworks
141}
142
143fn detect_databases(metadata: &Metadata) -> Vec<DetectedItem> {
144    let mut databases = Vec::new();
145    if has_dep_or_package(metadata, "rustauth-sqlx") || has_dep_or_package(metadata, "sqlx") {
146        databases.push(detected("sqlx", DetectionConfidence::High));
147    }
148    if has_dep_or_package(metadata, "rustauth-tokio-postgres") {
149        databases.push(detected("tokio-postgres", DetectionConfidence::High));
150    }
151    if has_dep_or_package(metadata, "rustauth-deadpool-postgres") {
152        databases.push(detected("deadpool-postgres", DetectionConfidence::High));
153    }
154    if has_dep_or_package(metadata, "rustauth-diesel") {
155        databases.push(detected("diesel", DetectionConfidence::High));
156    }
157    databases
158}
159
160fn detected(name: &str, confidence: DetectionConfidence) -> DetectedItem {
161    DetectedItem {
162        name: name.to_owned(),
163        confidence,
164    }
165}
166
167pub fn package_has_dependency(info: &WorkspaceInfo, dependency: &str) -> bool {
168    info.packages
169        .iter()
170        .any(|package| package.dependencies.iter().any(|name| name == dependency))
171}
172
173#[cfg(test)]
174#[allow(clippy::expect_used)]
175mod tests {
176    use super::*;
177    use std::fs;
178    use tempfile::TempDir;
179
180    fn write_manifest(dir: &TempDir, manifest: &str) {
181        fs::create_dir_all(dir.path().join("src")).expect("create src");
182        fs::write(dir.path().join("src/lib.rs"), "").expect("write lib");
183        fs::write(dir.path().join("Cargo.toml"), manifest).expect("write manifest");
184    }
185
186    fn inspect_manifest(dir: &TempDir) -> WorkspaceInfo {
187        inspect(dir.path()).expect("inspect workspace")
188    }
189
190    #[test]
191    fn detects_actix_web_with_high_confidence_when_rustauth_adapter_present() {
192        let dir = TempDir::new().expect("tempdir");
193        write_manifest(
194            &dir,
195            r#"
196[package]
197name = "app"
198version = "0.1.0"
199edition = "2021"
200
201[dependencies]
202actix-web = "4"
203rustauth-actix-web = "0.2"
204"#,
205        );
206
207        let info = inspect_manifest(&dir);
208        let actix = info
209            .detected_frameworks
210            .iter()
211            .find(|item| item.name == "actix-web")
212            .expect("actix-web detection");
213
214        assert_eq!(actix.confidence, DetectionConfidence::High);
215    }
216
217    #[test]
218    fn detects_actix_web_with_medium_confidence_without_rustauth_adapter() {
219        let dir = TempDir::new().expect("tempdir");
220        write_manifest(
221            &dir,
222            r#"
223[package]
224name = "app"
225version = "0.1.0"
226edition = "2021"
227
228[dependencies]
229actix-web = "4"
230"#,
231        );
232
233        let info = inspect_manifest(&dir);
234        let actix = info
235            .detected_frameworks
236            .iter()
237            .find(|item| item.name == "actix-web")
238            .expect("actix-web detection");
239
240        assert_eq!(actix.confidence, DetectionConfidence::Medium);
241    }
242}