Skip to main content

actr_cli/
plugin_config.rs

1use serde::Deserialize;
2use std::cmp::Ordering;
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::error::{ActrCliError, Result};
7
8const CONFIG_FILE_NAME: &str = ".protoc-plugin.toml";
9
10#[derive(Debug, Deserialize)]
11struct ProtocPluginFile {
12    version: Option<u32>,
13    plugins: Option<HashMap<String, String>>,
14}
15
16#[derive(Debug, Clone)]
17pub struct ProtocPluginConfig {
18    path: PathBuf,
19    plugins: HashMap<String, String>,
20}
21
22impl ProtocPluginConfig {
23    pub fn min_version(&self, plugin: &str) -> Option<&str> {
24        self.plugins.get(plugin).map(String::as_str)
25    }
26
27    pub fn path(&self) -> &Path {
28        &self.path
29    }
30}
31
32pub fn load_protoc_plugin_config(config_path: &Path) -> Result<Option<ProtocPluginConfig>> {
33    let config_dir = config_path
34        .parent()
35        .filter(|p| !p.as_os_str().is_empty())
36        .unwrap_or_else(|| Path::new("."));
37    let plugin_path = config_dir.join(CONFIG_FILE_NAME);
38    if !plugin_path.exists() {
39        return Ok(None);
40    }
41
42    let contents = std::fs::read_to_string(&plugin_path)?;
43    let parsed: ProtocPluginFile = toml::from_str(&contents).map_err(|e| {
44        ActrCliError::config_error(format!("Failed to parse {}: {e}", plugin_path.display()))
45    })?;
46
47    if let Some(version) = parsed.version
48        && version != 1
49    {
50        return Err(ActrCliError::config_error(format!(
51            "Unsupported .protoc-plugin.toml version {version} (expected 1)"
52        )));
53    }
54
55    let plugins = parsed.plugins.unwrap_or_default();
56    for (name, min_version) in &plugins {
57        if min_version.trim().is_empty() {
58            return Err(ActrCliError::config_error(format!(
59                "Minimum version for plugin '{name}' cannot be empty"
60            )));
61        }
62        if !is_valid_version_string(min_version) {
63            return Err(ActrCliError::config_error(format!(
64                "Invalid minimum version '{min_version}' for plugin '{name}'"
65            )));
66        }
67    }
68
69    Ok(Some(ProtocPluginConfig {
70        path: plugin_path,
71        plugins,
72    }))
73}
74
75pub fn compare_versions(v1: &str, v2: &str) -> Ordering {
76    let parse_version = |v: &str| -> Vec<u32> {
77        v.split('.')
78            .map(|s| s.parse::<u32>().unwrap_or(0))
79            .collect()
80    };
81
82    let v1_parts = parse_version(v1);
83    let v2_parts = parse_version(v2);
84    let max_len = v1_parts.len().max(v2_parts.len());
85    for i in 0..max_len {
86        let v1_part = v1_parts.get(i).copied().unwrap_or(0);
87        let v2_part = v2_parts.get(i).copied().unwrap_or(0);
88
89        match v1_part.cmp(&v2_part) {
90            Ordering::Equal => continue,
91            other => return other,
92        }
93    }
94
95    Ordering::Equal
96}
97
98pub fn version_is_at_least(candidate: &str, minimum: &str) -> bool {
99    compare_versions(candidate, minimum) != Ordering::Less
100}
101
102fn is_valid_version_string(value: &str) -> bool {
103    value.chars().all(|c| c.is_ascii_digit() || c == '.')
104        && value.chars().any(|c| c.is_ascii_digit())
105}