Skip to main content

openauth_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_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}