rustauth_cli/
workspace.rs1use 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}