openauth_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_openauth_axum = has_dep_or_package(metadata, "openauth-axum");
123 if has_axum && has_openauth_axum {
124 frameworks.push(detected("axum", DetectionConfidence::High));
125 } else if has_axum {
126 frameworks.push(detected("axum", DetectionConfidence::Medium));
127 }
128 for framework in ["actix-web", "rocket", "poem", "warp"] {
129 if has_dep_or_package(metadata, framework) {
130 frameworks.push(detected(framework, DetectionConfidence::Low));
131 }
132 }
133 frameworks
134}
135
136fn detect_databases(metadata: &Metadata) -> Vec<DetectedItem> {
137 let mut databases = Vec::new();
138 if has_dep_or_package(metadata, "openauth-sqlx") || has_dep_or_package(metadata, "sqlx") {
139 databases.push(detected("sqlx", DetectionConfidence::High));
140 }
141 if has_dep_or_package(metadata, "openauth-tokio-postgres") {
142 databases.push(detected("tokio-postgres", DetectionConfidence::High));
143 }
144 if has_dep_or_package(metadata, "openauth-deadpool-postgres") {
145 databases.push(detected("deadpool-postgres", DetectionConfidence::High));
146 }
147 databases
148}
149
150fn detected(name: &str, confidence: DetectionConfidence) -> DetectedItem {
151 DetectedItem {
152 name: name.to_owned(),
153 confidence,
154 }
155}
156
157pub fn package_has_dependency(info: &WorkspaceInfo, dependency: &str) -> bool {
158 info.packages
159 .iter()
160 .any(|package| package.dependencies.iter().any(|name| name == dependency))
161}